From 3983348e03e499a399f75596b7ce3012b828f831 Mon Sep 17 00:00:00 2001 From: Lexus Drumgold Date: Sun, 10 Oct 2021 02:54:07 -0400 Subject: [PATCH] feat(utils): `addSourceMap` --- .eslintignore | 1 + .eslintrc.base.cjs | 1 + .eslintrc.cjs | 6 +- .prettierignore | 1 + __tests__/__fixtures__/filenames.fixture.ts | 25 +++++ .../__fixtures__/trext-file-result.fixture.ts | 91 +++++++++++++++ __tests__/__fixtures__/trext.plugin.js | 104 ++++++++++++++++++ src/utils/__mocks__/add-source-map.util.ts | 9 ++ src/utils/__mocks__/ignore404.util.ts | 9 ++ src/utils/__mocks__/save-file.util.ts | 7 ++ .../__mocks__/source-mapping-url.util.ts | 9 ++ .../add-source-map.util.functional.spec.ts | 102 +++++++++++++++++ src/utils/add-source-map.util.ts | 53 +++++++++ src/utils/index.ts | 1 + 14 files changed, 418 insertions(+), 1 deletion(-) create mode 100644 __tests__/__fixtures__/filenames.fixture.ts create mode 100644 __tests__/__fixtures__/trext-file-result.fixture.ts create mode 100644 __tests__/__fixtures__/trext.plugin.js create mode 100644 src/utils/__mocks__/add-source-map.util.ts create mode 100644 src/utils/__mocks__/ignore404.util.ts create mode 100644 src/utils/__mocks__/save-file.util.ts create mode 100644 src/utils/__mocks__/source-mapping-url.util.ts create mode 100644 src/utils/__tests__/add-source-map.util.functional.spec.ts create mode 100644 src/utils/add-source-map.util.ts diff --git a/.eslintignore b/.eslintignore index 35568da..6cbea04 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,5 +9,6 @@ **/types/* **/CHANGELOG.md +__tests__/__fixtures__/trext.plugin.js !/src/types/* diff --git a/.eslintrc.base.cjs b/.eslintrc.base.cjs index e18b6c2..aff7c6e 100644 --- a/.eslintrc.base.cjs +++ b/.eslintrc.base.cjs @@ -134,6 +134,7 @@ module.exports = { 'ttsc', 'typeof', 'usr', + 'utf', 'wasm', 'wip', 'yargs' diff --git a/.eslintrc.cjs b/.eslintrc.cjs index c70c996..fef6dc1 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -19,6 +19,7 @@ module.exports = { ...RULES_SPELLCHECKER[1].skipWords, 'callee', 'errno', + 'filenames', 'trext', 'trextel' ] @@ -28,7 +29,10 @@ module.exports = { overrides: [ ...overrides, { - files: ['src/plugins/trextel.plugin.ts'], + files: [ + '__tests__/__fixtures__/trext-file-result.fixture.ts', + 'src/plugins/trextel.plugin.ts' + ], rules: { 'no-useless-escape': 0 } diff --git a/.prettierignore b/.prettierignore index 66b9503..b848b0a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,5 +20,6 @@ **/.prettierignore **/*.markdownlintignore **/yarn.lock +__tests__/__fixtures__/trext.plugin.js !/src/types/* diff --git a/__tests__/__fixtures__/filenames.fixture.ts b/__tests__/__fixtures__/filenames.fixture.ts new file mode 100644 index 0000000..7d62d25 --- /dev/null +++ b/__tests__/__fixtures__/filenames.fixture.ts @@ -0,0 +1,25 @@ +/** + * @file Test Fixture - filenames + * @module tests/fixtures/filenames + */ + +export default [ + '__tests__/__fixtures__/trext.plugin.js', + 'config/defaults.config.js', + 'interfaces/index.js', + 'interfaces/trext-options.interface.js', + 'plugins/trexel.plugin.js', + 'plugins/index.js', + 'types/file-extension.type.js', + 'types/index.js', + 'types/regex-string.type.js', + 'types/source-map-comment.type.js', + 'types/trext-defaults.type.js', + 'types/trext-to-fn.type.js', + 'types/trext-to.type.js', + 'utils/ignore-404.util.js', + 'utils/index.js', + 'utils/save-file.util.js', + 'utils/souce-mapping-url.util.js', + 'index.js' +] diff --git a/__tests__/__fixtures__/trext-file-result.fixture.ts b/__tests__/__fixtures__/trext-file-result.fixture.ts new file mode 100644 index 0000000..3f97eda --- /dev/null +++ b/__tests__/__fixtures__/trext-file-result.fixture.ts @@ -0,0 +1,91 @@ +import DEFAULTS from '@trext/config/defaults.config' +import type { TrextFileResult } from '@trext/types' +import fs from 'fs' +import FILENAMES from './filenames.fixture' + +/** + * @file Test Fixture - TrextFileResult + * @module tests/fixtures/TrextFileResult + */ + +export const SOURCE_FILENAME: string = FILENAMES[0] +export const CWD: string = process.cwd() +export const FILENAME: string = `${CWD}/${SOURCE_FILENAME}` + +const SOURCE_CODE = fs.readFileSync(FILENAME, 'utf8').toString() + +export default { + ast: null, + code: SOURCE_CODE, + map: { + file: SOURCE_FILENAME, + mappings: + // eslint-disable-next-line spellcheck/spell-checker + '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', + names: [], + sourceRoot: '', + sources: ['../src/trext.ts'], + sourcesContent: [ + `${SOURCE_CODE}\n"./config/defaults.config""./interfaces""./plugins/trextel.plugin""./types"` + ], + version: 3 + }, + metadata: {}, + options: { + assumptions: {}, + babelrc: false, + browserslistConfigFile: false, + caller: { name: '@babel/cli' }, + cloneInputAst: true, + configFile: false, + cwd: CWD, + envName: 'development', + filename: FILENAME, + generatorOpts: { + auxiliaryCommentAfter: undefined, + auxiliaryCommentBefore: undefined, + comments: true, + compact: 'auto', + filename: FILENAME, + minified: undefined, + retainLines: undefined, + shouldPrintComment: undefined, + sourceFileName: SOURCE_FILENAME, + sourceMaps: true, + sourceRoot: undefined + }, + parserOpts: { + plugins: [], + sourceFileName: FILENAME, + sourceType: 'module' + }, + passPerPreset: false, + plugins: [ + { + generatorOverride: undefined, + key: 'Trextel', + manipulateOptions: undefined, + options: { + ...DEFAULTS, + from: 'js', + to: 'cjs' + }, + parserOverride: undefined, + post: undefined, + pre: undefined, + visitor: { + _exploded: true, + _verified: true, + CallExpression: { enter: [[jest.fn()]] }, + ImportDeclaration: { enter: [[jest.fn()]] } + } + } + ], + presets: [], + root: CWD, + rootMode: 'root', + sourceMaps: true, + targets: {} + }, + sourceType: 'module' +} as TrextFileResult diff --git a/__tests__/__fixtures__/trext.plugin.js b/__tests__/__fixtures__/trext.plugin.js new file mode 100644 index 0000000..2915158 --- /dev/null +++ b/__tests__/__fixtures__/trext.plugin.js @@ -0,0 +1,104 @@ +import { transformFileAsync } from '@babel/core'; +import fs from 'fs/promises'; +import DEFAULTS from "../config/defaults.config.mjs"; +import addSourceMap from "../utils/add-source-map.util.mjs"; +import glob from "../utils/glob.util.mjs"; +import saveFile from "../utils/save-file.util.mjs"; +import Trextel from "./trextel.plugin.mjs"; +/** + * @file Plugins - Trext + * @module trext/plugins/Trext + */ + +/** + * File extension and import statement transformer. + */ + +class Trext { + /** + * Executes `trextFile` over a directory. + * + * @see {@link Trext.trextFile} + * + * @template F - Old file extension name(s) + * @template T - New file extension name(s) + * + * @async + * @param {string} cwd - Directory to perform transformation + * @param {TrextOptions} options - Trext options + * @param {TransformOptions} [options.babel={}] - Babel transform options + * @param {F} options.from - File extension to transform + * @param {RegexString} [options.pattern=/\..+$/] - File extension pattern + * @param {To} options.to - New file extension or generator function + * @return {Promise} Transformation results + */ + static async trext(cwd, options) { + // Get files to transform + const filenames = await glob(`${cwd}/**/*.${options.from}`); // Transform files + + const results = filenames.map(async f => Trext.trextFile(f, options)); // Return transformation results + + return await Promise.all(results); + } + /** + * Transforms a file's extension, import statements, call expressions, and + * source map comment. + * + * @template F - Old file extension name(s) + * @template T - New file extension name(s) + * + * @async + * @param {string} filename - Name of file to transform + * @param {TrextOptions} options - Trext options + * @param {TransformOptions} [options.babel={}] - Babel transform options + * @param {F} options.from - File extension to transform + * @param {RegexString} [options.pattern=/\..+$/] - File extension pattern + * @param {To} options.to - New file extension or generator function + * @return {Promise} Transformation result + * @throws {Error} + */ + + + static async trextFile(filename, options) { + // Merge user options with defaults + const opts = { ...DEFAULTS, + ...options + }; + const { + babel, + pattern, + to + } = opts; // Transform file + + let result = await transformFileAsync(filename, { ...Object.assign({}, { ...DEFAULTS.babel, + ...babel + }), + caller: { + name: '@babel/cli' + }, + plugins: [[new Trextel().plugin, opts], ...(babel?.plugins ?? [])] + }); // Throw error if result is missing + + if (!result) throw new Error(`Could not compile file ${filename}`); // Get output filename + // @ts-expect-error No overload matches this call + + const output = filename.replace(pattern, to.call ? to : `.${to}`); // Stringify source code + + result.code = String(result.code); // Handle source map + + result = await addSourceMap(opts, result, { + input: filename, + output + }); // Save file, update file permissions, and remove references to old file + + await saveFile(output, result.code); + await fs.chmod(output, (await fs.stat(filename)).mode); + await fs.unlink(filename); + return result; + } + +} + +export const trext = Trext.trext; +export const trextFile = Trext.trextFile; +export default Trext; \ No newline at end of file diff --git a/src/utils/__mocks__/add-source-map.util.ts b/src/utils/__mocks__/add-source-map.util.ts new file mode 100644 index 0000000..54b4087 --- /dev/null +++ b/src/utils/__mocks__/add-source-map.util.ts @@ -0,0 +1,9 @@ +/** + * @file User Module Mock - addSourceMap + * @module utils/mocks/addSourceMap + * @see https://jestjs.io/docs/next/manual-mocks#mocking-user-modules + */ + +export default jest.fn((...args) => { + return jest.requireActual('../add-source-map.util').default(...args) +}) diff --git a/src/utils/__mocks__/ignore404.util.ts b/src/utils/__mocks__/ignore404.util.ts new file mode 100644 index 0000000..2f7f07a --- /dev/null +++ b/src/utils/__mocks__/ignore404.util.ts @@ -0,0 +1,9 @@ +/** + * @file User Module Mock - ignore404 + * @module utils/mocks/ignore404 + * @see https://jestjs.io/docs/next/manual-mocks#mocking-user-modules + */ + +export default jest.fn((...args) => { + return jest.requireActual('../ignore-404.util').default(...args) +}) diff --git a/src/utils/__mocks__/save-file.util.ts b/src/utils/__mocks__/save-file.util.ts new file mode 100644 index 0000000..070e59d --- /dev/null +++ b/src/utils/__mocks__/save-file.util.ts @@ -0,0 +1,7 @@ +/** + * @file User Module Mock - saveFile + * @module utils/mocks/saveFile + * @see https://jestjs.io/docs/next/manual-mocks#mocking-user-modules + */ + +export default jest.fn() diff --git a/src/utils/__mocks__/source-mapping-url.util.ts b/src/utils/__mocks__/source-mapping-url.util.ts new file mode 100644 index 0000000..3590ca5 --- /dev/null +++ b/src/utils/__mocks__/source-mapping-url.util.ts @@ -0,0 +1,9 @@ +/** + * @file User Module Mock - sourceMappingURL + * @module utils/mocks/sourceMappingURL + * @see https://jestjs.io/docs/next/manual-mocks#mocking-user-modules + */ + +export default jest.fn((...args) => { + return jest.requireActual('../source-mapping-url.util').default(...args) +}) diff --git a/src/utils/__tests__/add-source-map.util.functional.spec.ts b/src/utils/__tests__/add-source-map.util.functional.spec.ts new file mode 100644 index 0000000..d125b5f --- /dev/null +++ b/src/utils/__tests__/add-source-map.util.functional.spec.ts @@ -0,0 +1,102 @@ +import TFRES, { FILENAME } from '@tests/fixtures/trext-file-result.fixture' +import type { TrextOptions } from '@trext/interfaces' +import type { TrextFileResult } from '@trext/types' +import ignore404 from '@trext/utils/ignore-404.util' +import saveFile from '@trext/utils/save-file.util' +import sourceMappingURL from '@trext/utils/source-mapping-url.util' +import fs from 'fs/promises' +import path from 'path' +import testSubject from '../add-source-map.util' + +/** + * @file Functional Tests - addSourceMap + * @module trext/utils/tests/unit/addSourceMap + */ + +jest.mock('@trext/utils/ignore-404.util') +jest.mock('@trext/utils/save-file.util') +jest.mock('@trext/utils/source-mapping-url.util') +jest.mock('fs/promises') + +const mockFs = fs as jest.Mocked +const mockIgnore404 = ignore404 as jest.MockedFunction +const mockSaveFile = saveFile as jest.MockedFunction +const mockSourceMappingURL = sourceMappingURL as jest.MockedFunction< + typeof sourceMappingURL +> + +describe('functional:utils/addSourceMap', () => { + const RESULT: TrextFileResult = Object.assign({}, TFRES) + + describe('comments', () => { + const options: TrextOptions<'js', 'mjs'> = { + babel: { sourceMaps: true }, + from: 'js', + to: 'mjs' + } + const output = FILENAME.replace(options.from, options.to as string) + const output_map = `${output}.map` + + beforeEach(async () => { + await testSubject(options, RESULT, { input: FILENAME, output }) + }) + + it('should add source map comment', () => { + expect(mockSourceMappingURL).toBeCalledTimes(1) + expect(mockSourceMappingURL).toBeCalledWith(output_map) + }) + + it('should update source map filename', () => { + expect(RESULT.map.file).toBe(path.basename(output)) + }) + }) + + describe('files', () => { + const OPTIONS: Pick, 'from' | 'to'> = { + from: 'js', + to: 'cjs' + } + + const OUTPUT = FILENAME.replace(OPTIONS.from, OPTIONS.to as string) + const FILENAMES = { input: FILENAME, output: OUTPUT } + + it('should create source map file with new extension', async () => { + // Arrange + const options: TrextOptions<'js', 'cjs'> = { + ...OPTIONS, + babel: { sourceMaps: true } + } + + const output = FILENAME.replace(options.from, options.to as string) + const output_map = `${output}.map` + const input_map = `${FILENAME}.map` + + // Act + await testSubject(options, RESULT, FILENAMES) + + // Expect + expect(mockSaveFile).toBeCalledTimes(1) + expect(mockSaveFile).toBeCalledWith(output_map, expect.any(String)) + expect(mockFs.unlink).toBeCalledTimes(1) + expect(mockFs.unlink).toBeCalledWith(input_map) + expect(mockIgnore404).toBeCalledTimes(1) + expect(mockIgnore404).toBeCalledWith(mockFs.unlink(input_map)) + }) + + it('should not create file if source map is inline', async () => { + // Arrange + const options: TrextOptions<'js', 'cjs'> = { + ...OPTIONS, + babel: { sourceMaps: 'inline' as const } + } + + // Act + await testSubject(options, RESULT, FILENAMES) + + // Expect + expect(mockSaveFile).toBeCalledTimes(0) + expect(mockFs.unlink).toBeCalledTimes(0) + expect(mockIgnore404).toBeCalledTimes(0) + }) + }) +}) diff --git a/src/utils/add-source-map.util.ts b/src/utils/add-source-map.util.ts new file mode 100644 index 0000000..ecbf3d5 --- /dev/null +++ b/src/utils/add-source-map.util.ts @@ -0,0 +1,53 @@ +import { TrextOptions } from '@trext/interfaces' +import { TrextFileResult } from '@trext/types' +import fs from 'fs/promises' +import path from 'path' +import ignore404 from './ignore-404.util' +import saveFile from './save-file.util' +import sourceMappingURL from './source-mapping-url.util' + +/** + * @file Utilities - addSourceMap + * @module trext/utils/addSourceMap + */ + +/** + * Adds a `SourceMap` comment to `result.code` if source maps are enabled. + * + * @template F - Old file extension name(s) + * @template T - New file extension name(s) + * + * @async + * @param {TrextOptions} options - Trext options + * @param {TrextFileResult} result - File transformation result + * @param {{input:string;output:string}} filenames - Filenames map + * @return {Promise} File transformation result + */ +async function addSourceMap< + F extends string = string, + T extends string = string +>( + options: TrextOptions, + result: TrextFileResult, + filenames: Record<'input' | 'output', string> +): Promise { + // Do nothing if source maps aren't enabled + if (!(result.map && options.babel?.sourceMaps)) return result + + // Get source map filename + const filename = `${filenames.output}.map` + + // Add source map comment and update source map filename + result.code = `${result.code}\n${sourceMappingURL(filename)}\n` + result.map.file = path.basename(filenames.output) + + // Save source map and remove references to old source map + if (options.babel.sourceMaps !== 'inline') { + await saveFile(filename, JSON.stringify(result.map)) + await ignore404(fs.unlink(`${filenames.input}.map`)) + } + + return result +} + +export default addSourceMap diff --git a/src/utils/index.ts b/src/utils/index.ts index 78db0f7..f5c0519 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,6 +3,7 @@ * @module trext/utils */ +export { default as addSourceMap } from './add-source-map.util' export { default as ignore404 } from './ignore-404.util' export { default as saveFile } from './save-file.util' export { default as sourceMappingURL } from './source-mapping-url.util'