我正在尝试向现有的 javascript 代码注入注释。
在 Babel 中,有
addComment
助手,可以在 AST 节点上调用。
这是一个用 Babel 编写的简单(但相当愚蠢)的转换:
console.log`Hello, ${name}!`;
-> /*#__PURE__*/console.log("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.log`Hello, ${name}!`;";
console.log(run(code));
// -> /*#__PURE__*/console.log("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,
{
理想情况下,测试应该反映代码在生产中的使用方式。添加仅用于测试的泛型类型参数会使代码更难以阅读,并且对我来说感觉不对。
有更好的方法吗?
SWC 作者在此。
您可以使您的转换器变得通用
C: Comments
,就像官方的pure_annotations pass一样。然后像其他仿制药一样存储 C
。
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
pass 使用此模式来减少二进制大小。在这种情况下,您应该在构造函数返回类型中的 '_
和 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,
})
}