Я пытаюсь добавить комментарий к существующему коду JavaScript.
В Babel есть помощник addComment
, который можно вызвать на узле AST.
Вот простое (но довольно глупое) преобразование, написанное на Babel:
console.info`Hello, ${name}!`;
-> /*#__PURE__*/console.info("Hello, ", name, "!");
const babel = require("@babel/core");
function run(code) {
const result = babel.transform(code, {
plugins: [
function transform({ types: t }) {
return {
visitor: {
TaggedTemplateExpression(path) {
const { tag, quasi } = path.node;
if (
t.isMemberExpression(tag) &&
t.isIdentifier(tag.object, { name: "console" }) &&
t.isIdentifier(tag.property, { name: "log" })
) {
let args = [];
quasi.quasis.forEach((element, index) => {
args.push(t.stringLiteral(element.value.raw));
if (index < quasi.expressions.length) {
args.push(quasi.expressions[index]);
}
});
path.replaceWith(
t.callExpression(
t.addComment(
t.memberExpression(
t.identifier("console"),
t.identifier("log")
),
"leading",
"#__PURE__"
),
args
)
);
}
},
},
};
},
],
});
return result.code;
}
const code = "console.info`Hello, ${name}!`;";
console.info(run(code));
// -> /*#__PURE__*/console.info("Hello, ", name, "!");
Однако в Rust все немного сложнее, поскольку только одна переменная может владеть данными, и на нее может быть не более одной изменяемой ссылки, кроме того, в SWC реализованы некоторые трюки с производительностью.
Таким образом, в SWC вам необходимо использовать PluginCommentsProxy , который описан в текущей версии SWC 0.279.0 с помощью следующей блок-схемы:
Below diagram shows one reference example how guest does trampoline between
host's memory space.
┌───────────────────────────────────────┐ ┌─────────────────────────────────────────────┐
│Host (SWC/core) │ │Plugin (wasm) │
│ ┌────────────────────────────────┐ │ │ │
│ │COMMENTS.with() │ │ │ ┌──────────────────────────────────────┐ │
│ │ │ │ │ │PluginCommentsProxy │ │
│ │ │ │ │ │ │ │
│ │ ┌────────────────────────────┐ │ │ │ │ ┌────────────────────────────────┐ │ │
│ │ │get_leading_comments_proxy()│◀┼───┼────┼──┼─┤get_leading() │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ ┌──────────────────────────┐ │ │ │
│ │ │ │─┼───┼──┬─┼──┼─┼─▶AllocatedBytesPtr(p,len) │ │ │ │
│ │ └────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ └─────────────┬────────────┘ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ ┌─────────────▼────────────┐ │ │ │
│ └────────────────────────────────┘ │ │ │ │ │ │Vec<Comments> │ │ │ │
│ │ └─┼──┼─┼─▶ │ │ │ │
│ │ │ │ │ └──────────────────────────┘ │ │ │
│ │ │ │ └────────────────────────────────┘ │ │
│ │ │ └──────────────────────────────────────┘ │
└───────────────────────────────────────┘ └─────────────────────────────────────────────┘
1. Plugin calls `PluginCommentsProxy::get_leading()`. PluginCommentsProxy is
a struct constructed in plugin's memory space.
2. `get_leading()` internally calls `__get_leading_comments_proxy`, which is
imported fn `get_leading_comments_proxy` exists in the host.
3. Host access necessary values in its memory space (COMMENTS)
4. Host copies value to be returned into plugin's memory space. Memory
allocation for the value should be manually performed.
5. Host completes imported fn, `PluginCommentsProxy::get_leading()` now can
read, deserialize memory host wrote.
- In case of `get_leading`, returned value is non-deterministic vec
(`Vec<Comments>`) guest cannot preallocate with specific length. Instead,
guest passes a fixed size struct (AllocatedBytesPtr), once host allocates
actual vec into guest it'll write pointer to the vec into the struct.
comments.add_leading(
node.span.lo,
Comment {
kind: swc_core::common::comments::CommentKind::Block,
span: DUMMY_SP,
text: "#__PURE__".to_string(),
},
)
К сожалению, мне не удалось протестировать его должным образом с помощью swc_core::ecma::transforms::testing
.
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use swc_core::ecma::transforms::testing::{test_fixture};
use swc_ecma_transforms_testing::{FixtureTestConfig};
#[testing::fixture("tests/fixture/**/input.tsx")]
fn fixture(input: PathBuf) {
test_fixture(
Default::default(),
&|tester| as_folder(TransformVisitor::new(&tester.comments)),
&input,
&input.with_file_name("output.tsx"),
FixtureTestConfig::default(),
);
}
}
К сожалению, это не работает, потому что tester.comments
относится к типу Rc<SingleThreadedComments>
.
Я видел примеры использования <C>
, например, трансформатора MillionJs:
fn transform_block<C>(context: &ProgramStateContext, node: &mut CallExpr, comments: C)
where
C: Comments,
{
В идеале тесты должны отражать то, как код будет использоваться в производстве. Добавление параметра универсального типа только для тестирования усложняет чтение кода и кажется мне неправильным.
Есть ли способ лучше?
🤔 А знаете ли вы, что...
JavaScript является одним из трех основных языков веб-разработки, вместе с HTML и CSS.
Автор SWC здесь.
Вы можете сделать свой трансформер более универсальным C: Comments
, как в официальном проходе pure_annotations. Затем сохраните C
так же, как и другие дженерики.
Вы можете использовать PluginCommentProxy
из плагина Wasm даже во время тестирования, если запускаете тесты через swc_ecma_transforms_testing
или swc_core::ecma::transforms::testing
, используя такой метод, как test_fixture
.
PluginCommentProxy.add_leading(n.span.lo, Comment {
// ...fields
});
будет работать только во время тестирования.
Это было необходимо раньше https://github.com/swc-project/swc/pull/9150, но этот способ работает до сих пор.
struct PureAnnotations<C>
where
C: Comments,
{
imports: AHashMap<Id, (JsWord, JsWord)>,
comments: Option<C>,
}
после этого вам следует сделать раздел impl универсальным поверх C
.
impl<C> VisitMut for PureAnnotations<C>
where
C: Comments,
{
noop_visit_mut_type!();
}
Вы можете добавить подходящие методы посетителей для достижения своей цели.
Альтернативно вы можете принять &dyn Comments
или Option<&dyn Comments>
. Официальный проход fixer использует этот шаблон для уменьшения размера двоичного файла. В этом случае вам следует добавить '_
между impl
и Fold
в возвращаемом типе конструктора.
pub fn fixer(comments: Option<&dyn Comments>) -> impl '_ + Fold + VisitMut {
as_folder(Fixer {
comments,
ctx: Default::default(),
span_map: Default::default(),
in_for_stmt_head: Default::default(),
in_opt_chain: Default::default(),
remove_only: false,
})
}