Skip to content

Commit

Permalink
Merge pull request #18 from danmooozi/refactor_transform
Browse files Browse the repository at this point in the history
transform 동작 설명 추가 및 테스트 유틸함수 수정
  • Loading branch information
Danji-ya authored Dec 10, 2023
2 parents 01f69e4 + 096b9d6 commit 7dfedf3
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 56 deletions.
26 changes: 13 additions & 13 deletions src/transform/constants/index.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
export const FORMAT = {
ESM: 'esm',
CJS: 'cjs',
};
import transformStrictMode from '../transform-strict-mode.js';
import transformEsmToCjs from '../transform-esm-to-cjs.js';

export const DEFAULT_OPTIONS = {
requireCode: true,
requireAst: false,
format: FORMAT.CJS,
};

export const DEFAULT_IMPORT_KEYWORD = 'default';
export const DEFAULT_IMPORT_KEYWORD = '_default';

// aliases
Object.assign(FORMAT, {
esm: FORMAT.ESM,
es: FORMAT.ESM,
cjs: FORMAT.CJS,
cj: FORMAT.CJS,
});
export const PLUGIN_MAP = {
strictMode: {
plugin: transformStrictMode,
conditions: [],
},
esmToCjs: {
plugin: transformEsmToCjs,
conditions: [],
},
};
20 changes: 6 additions & 14 deletions src/transform/index.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import { transformFromAstSync } from '@babel/core';
import transformEsmToCjs from './transform-esm-to-cjs.js';
import transformStrictMode from './transform-strict-mode.js';
import { DEFAULT_OPTIONS, FORMAT } from './constants/index.js';
import { DEFAULT_OPTIONS, PLUGIN_MAP } from './constants/index.js';

const getActualOptions = (options) =>
Object.assign({}, DEFAULT_OPTIONS, options);

const PLUGIN_MAP = {
strictMode: {
plugin: transformStrictMode,
conditions: [],
},
esmToCjs: {
plugin: transformEsmToCjs,
conditions: [({ format }) => FORMAT[format] === FORMAT.CJS],
},
};

const transform = (ast, content, options) => {
const { requireAst, requireCode, ...restOptions } = getActualOptions(options);

/*
* babel.transformFromAstSync
* 변환 단계에서는 추상 구문 트리(AST)를 받아 그 속을 탐색해 나가며 노드들을 추가, 업데이트, 제거
* {@link https://babeljs.io/docs/babel-core#transformfromastsync}
*/
const { ast: transformedAst, code: transformedContent } =
transformFromAstSync(ast, content, {
ast: requireAst,
Expand Down
7 changes: 4 additions & 3 deletions src/transform/test/index.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { format, contentToTransformedContent } from './utils';
import { DEFAULT_IMPORT_KEYWORD } from '../constants';

describe('import declaration', () => {
it('can support Default import', () => {
Expand All @@ -11,7 +12,7 @@ describe('import declaration', () => {
"use strict";
const _imported = require("./square");
console.log("Area of square: ", _imported["default"](3, 5));
console.log("Area of square: ", _imported["${DEFAULT_IMPORT_KEYWORD}"](3, 5));
`;

expect(contentToTransformedContent(content)).toBe(
Expand Down Expand Up @@ -49,7 +50,7 @@ describe('ExportDefaultDeclaration', () => {
const expectedTransformedContent = format`
"use strict";
module.exports.default = test;
module.exports.${DEFAULT_IMPORT_KEYWORD} = test;
function test(user) {
console.log(user);
}
Expand All @@ -71,7 +72,7 @@ describe('ExportDefaultDeclaration', () => {
"use strict";
const test = 'test';
module.exports.default = test;
module.exports.${DEFAULT_IMPORT_KEYWORD} = test;
`;

expect(contentToTransformedContent(content)).toBe(
Expand Down
39 changes: 33 additions & 6 deletions src/transform/test/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,45 @@ import transform from '../../index.js';

const contentToAST = (content) => parseSync(content);

export const format = (strings) => {
const source = strings.join('').trim();
const lines = source.split('\n');
const getReplacementMap = (lines, values) => {
const replacements = new Map();
const usedValues = values;

lines.forEach((line) => {
const match = line.match(/\${(.*?)}/);
if (match) {
const placeholder = match[0];
const value = usedValues.shift();
replacements.set(placeholder, value);
}
});

return replacements;
};

const replacePlaceholderToValue = (source, replacements) => {
let replacedSource = source;

if (lines.length === 1) {
return source;
for (const [placeholder, value] of replacements) {
replacedSource = replacedSource.replace(placeholder, value);
}

return replacedSource;
};

export const format = (strings, ...values) => {
const source = String.raw(strings, ...values).trim();
const lines = source.split('\n');

if (lines.length === 1) return source;

const space = lines[lines.length - 1].match(/\s+/)[0];
const exp = new RegExp(`${space}`, 'g');

return source.replace(exp, '');
const replacements = getReplacementMap(lines, values);
const replacedSource = replacePlaceholderToValue(source, replacements);

return replacedSource.replace(exp, '');
};

export const contentToTransformedContent = (content) => {
Expand Down
80 changes: 61 additions & 19 deletions src/transform/transform-esm-to-cjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const kimbapVisitor = {
ExportNamedDeclaration: exportNamedDeclarationVisitor,
};

/*
* AST 변환을 하고 싶을때, 재귀적으로 트리를 탐색해야 한다.
* AST는 단일 노드 또는 수백개, 수천개의 노드로 이루어 질 수 있으며 각 노드들을 탐색하는 동안 막다른 곳에 다다르는데
* 이때, 다음 노드로 가기위해서는 뒤로 돌아가는 것이 필요하기 때문에 각 노드에 입장(enter)하고, 돌아올때 각 노드를 퇴장(exit)하면서 두번의 방문을 거치게 된다.
*/
const visitor = {
Program: {
enter(programPath, state) {
Expand All @@ -34,29 +39,43 @@ const visitor = {
};

function importDeclarationVisitor(path) {
/*
* 로컬로 정의된 변수와 충돌하지 않는 식별자 생성
* Node { type: "Identifier", name: "_imported" }
*/
const newIdentifier = path.scope.generateUidIdentifier('imported');

const specifiers = path.get('specifiers');

if (!isArray(specifiers)) return;

specifiers.forEach((specifier) => {
// import square from "./square";의 경우
// square에 해당하는 이름의 바인딩 목록들을 가져온다.
/*
* import specifier from "library" 의 경우
* specifier에 대한 바인딩 목록들을 가져온다.
*/
const { referencePaths } = specifier.scope.getBinding(
specifier.node.local.name,
);

// a라는 importedKey에 접근하기 위해서는 두 가지의 경우가 존재
// default export 의 형태인 경우 import a from "source"
// 아닌 경우 import { a } from "source"
/*
* a라는 importedKey에 접근하기 위해서는 두 가지의 경우가 존재
* default export 의 형태인 경우 import a from "source"
* 아닌 경우 import { a } from "source"
*/
const importedKey = specifier.isImportDefaultSpecifier()
? DEFAULT_IMPORT_KEYWORD
: specifier.get('imported.name').node;

// convert console.log(square(1, 2)) to console.log(newIdentifier[`importedKey`](1, 2))
/*
* 바인딩 목록들을 순회하며 노드를 업데이트 한다.
* specifier(1, 2)를 newIdentifier[`importedKey`](1, 2) 형태로 업데이트 한다.
*/
referencePaths.forEach((refPath) => {
// If computed === true, `object[property]`.
// Else, `object.property` -- meaning property should be an Identifier.
/*
* If computed === true, `object[property]`.
* Else, `object.property` -- meaning property should be an Identifier.
*/
refPath.replaceWith(
t.memberExpression(newIdentifier, t.stringLiteral(importedKey), true),
);
Expand All @@ -70,26 +89,33 @@ function importDeclarationVisitor(path) {
SOURCE: t.stringLiteral(path.get('source.value').node),
});

// const newIdentifier = require(`path.get("source.value").node`);
/*
* import문을 require문으로 업데이트 한다.
* const newIdentifier = require(`path.get("source.value").node`);
*/
path.replaceWith(newNode);
}

function exportDefaultDeclarationVisitor(path) {
const declaration = path.get('declaration');

// 함수선언문인 경우
/*
* 함수선언문인 경우
* module.exports = test;
* function test(){...}
*/
if (declaration.isFunctionDeclaration()) {
// module.exports = test;
// function test(){...}
path.replaceWithMultiple([
getModuleExportsAssignment(t.identifier(declaration.node.id.name)),
declaration.node,
]);
return;
}

// 나머지인 경우
// module.exports = test;
/*
* module.exports = test;
* export문을 module.exports문으로 업데이트 한다.
*/
path.replaceWith(
getModuleExportsAssignment(t.identifier(declaration.node.name)),
);
Expand All @@ -98,27 +124,37 @@ function exportDefaultDeclarationVisitor(path) {
function exportNamedDeclarationVisitor(path) {
const declarations = [];

// Exporting declarations
if (path.has('declaration')) {
const declaration = path.get('declaration');

/*
* 클래스선언문인 경우
* export class A {};
*/
if (declaration.isClassDeclaration()) {
declarations.push({
name: declaration.node.id,
value: t.toExpression(declaration.node),
});
}

/*
* 함수선언문인 경우
* export function func(){};
*/
if (declaration.isFunctionDeclaration()) {
declarations.push({
name: declaration.node.id,
value: t.toExpression(declaration.node),
});
}

/*
* 변수선언문인 경우
* export const foo = 'a';
* export const foo = 'a', bar = 'b';
*/
if (declaration.isVariableDeclaration()) {
// export const foo = 'a';
// export const foo = 'a', bar = 'b';
const decls = declaration.get('declarations');

decls.forEach((decl) => {
Expand All @@ -130,7 +166,10 @@ function exportNamedDeclarationVisitor(path) {
}
}

// Export list
/*
* Export list인 경우
* export { name1, …, nameN };
*/
if (path.has('specifiers')) {
const specifiers = path.get('specifiers');

Expand All @@ -142,7 +181,10 @@ function exportNamedDeclarationVisitor(path) {
});
}

// module.exports.[property] = value;
/*
* module.exports.[property] = value;
* export문을 module.exports문으로 업데이트 한다.
*/
path.replaceWithMultiple(
declarations.map((decl) =>
getModuleExportsAssignment(decl.value, decl.name),
Expand Down
10 changes: 9 additions & 1 deletion src/transform/transform-strict-mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@ const visitor = {
Program: programVisitor,
};

function programVisitor(path, state) {
function programVisitor(path, _state) {
const {
node: { directives },
} = path;

/*
* 'use strict' 가 존재하는 경우
* 따로 노드를 업데이트하지 않는다.
*/
for (const directive of directives) {
if (directive.value.value === 'use strict') return;
}

/*
* 'use strict' 가 존재하지 않는 경우
* use strict' 노드를 추가한다.
*/
path.unshiftContainer(
'directives',
t.directive(t.directiveLiteral('use strict')),
Expand Down

0 comments on commit 7dfedf3

Please sign in to comment.