Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changeset/social-nails-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---

---
58 changes: 58 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/react/transform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ crate-type = ["cdylib"]
convert_case = { workspace = true }
hex = { workspace = true }
indexmap = { workspace = true }
napi = { workspace = true }
napi = { workspace = true, features = ["serde-json"] }
napi-derive = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
Expand All @@ -25,6 +25,7 @@ swc_plugin_css_scope = { path = "./crates/swc_plugin_css_scope", features = ["na
swc_plugin_define_dce = { path = "./crates/swc_plugin_define_dce", features = ["napi"] }
swc_plugin_directive_dce = { path = "./crates/swc_plugin_directive_dce", features = ["napi"] }
swc_plugin_dynamic_import = { path = './crates/swc_plugin_dynamic_import', features = ["napi"] }
swc_plugin_element_template = { path = "./crates/swc_plugin_element_template", features = ["napi"] }
swc_plugin_inject = { path = "./crates/swc_plugin_inject", features = ["napi"] }
swc_plugin_list = { path = "./crates/swc_plugin_list", features = ["napi"] }
swc_plugin_shake = { path = './crates/swc_plugin_shake', features = ["napi"] }
Expand Down
110 changes: 110 additions & 0 deletions packages/react/transform/__test__/fixture.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,116 @@ describe('ui source map', () => {
});
});

describe('element template', () => {
it('should export compiled element templates when enabled', async () => {
const result = await transformReactLynx('const node = <view className="foo" />;', {
mode: 'test',
pluginName: '',
filename: 'test.js',
sourcemap: false,
cssScope: false,
elementTemplate: {
preserveJsx: false,
runtimePkg: '@lynx-js/react',
filename: 'test.js',
target: 'LEPUS',
},
jsx: true,
directiveDCE: false,
defineDCE: false,
shake: false,
compat: true,
worklet: false,
refresh: false,
});

expect(Array.isArray(result.elementTemplates)).toBe(true);
const template = result.elementTemplates?.[0];
expect(template?.templateId).toEqual(expect.any(String));
expect(template?.templateId.length).toBeGreaterThan(0);
expect(template?.compiledTemplate).toEqual(expect.any(Object));
expect(Array.isArray(template?.compiledTemplate)).toBe(false);
expect(template?.sourceFile).toEqual(expect.any(String));
});

it('should not bridge legacy snapshot ET flags into the new ET plugin path', async () => {
const result = await transformReactLynx('const node = <view className="foo" />;', {
mode: 'test',
pluginName: '',
filename: 'test.js',
sourcemap: false,
cssScope: false,
snapshot: {
preserveJsx: false,
runtimePkg: '@lynx-js/react',
filename: 'test.js',
target: 'LEPUS',
enableElementTemplate: true,
},
jsx: true,
directiveDCE: false,
defineDCE: false,
shake: false,
compat: true,
worklet: false,
refresh: false,
});

expect(result.elementTemplates).toBeUndefined();
});

it('should return an empty template list when element template emits no assets', async () => {
const result = await transformReactLynx('const value = 1;', {
mode: 'test',
pluginName: '',
filename: 'test.js',
sourcemap: false,
cssScope: false,
elementTemplate: {
preserveJsx: false,
runtimePkg: '@lynx-js/react',
filename: 'test.js',
target: 'LEPUS',
},
jsx: true,
directiveDCE: false,
defineDCE: false,
shake: false,
compat: true,
worklet: false,
refresh: false,
});

expect(result.elementTemplates).toEqual([]);
});

it('should warn when snapshot and element template are both explicitly enabled', async () => {
const result = await transformReactLynx('const node = <view />;', {
mode: 'test',
pluginName: '',
filename: 'test.js',
sourcemap: false,
cssScope: false,
snapshot: true,
elementTemplate: true,
jsx: true,
directiveDCE: false,
defineDCE: false,
shake: false,
compat: true,
worklet: false,
refresh: false,
});

expect(result.elementTemplates).toEqual(expect.any(Array));
expect(result.warnings).toEqual([
expect.objectContaining({
text: expect.stringContaining('elementTemplate'),
}),
]);
});
});

describe('jsx', () => {
it('should allow JSXNamespace', async () => {
const result = await transformReactLynx('const jsx = <Foo main-thread:foo={foo} />', {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[package]
name = "swc_plugin_element_template"
version = "0.1.0"
edition = "2021"

[lib]
path = "lib.rs"

[features]
napi = ["dep:napi", "dep:napi-derive", "swc_plugins_shared/napi"]

[dependencies]
convert_case = { workspace = true }
hex = { workspace = true }
napi = { workspace = true, optional = true, features = ["serde-json"] }
napi-derive = { workspace = true, optional = true }
once_cell = { workspace = true }
regex = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["preserve_order"] }
sha-1 = { workspace = true }
swc_core = { workspace = true, features = ["base", "ecma_codegen", "ecma_parser", "ecma_transforms_typescript", "ecma_utils", "ecma_quote", "ecma_transforms_react", "ecma_transforms_optimization", "ecma_visit", "testing_transform"] }
swc_plugins_shared = { path = "../swc_plugins_shared" }

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(swc_ast_unknown)'] }

[dev-dependencies]
insta = { version = "1.34", features = ["json"] }
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use serde::Serialize;
use swc_core::common::comments::Comments;

use super::JSXTransformer;

#[derive(Serialize, Debug, Clone)]
pub struct ElementTemplateAsset {
// The compiled template is exported out-of-band from the JS module. The JS
// output keeps using a synthetic component tag so existing React/SWC passes can
// continue to own expression lowering and runtime value transport.
pub template_id: String,
pub compiled_template: serde_json::Value,
pub source_file: String,
}

const BUILTIN_RAW_TEXT_TEMPLATE_ID: &str = "__et_builtin_raw_text__";
const BUILTIN_SOURCE_FILE: &str = "<builtin>";

impl<C> JSXTransformer<C>
where
C: Comments + Clone,
{
fn builtin_raw_text_template_asset(&self) -> ElementTemplateAsset {
ElementTemplateAsset {
template_id: BUILTIN_RAW_TEXT_TEMPLATE_ID.to_string(),
compiled_template: serde_json::json!({
"kind": "element",
"type": "raw-text",
"attributesArray": [
{
"kind": "attribute",
"key": "text",
"binding": "slot",
"attrSlotIndex": 0,
}
],
"children": [],
}),
source_file: BUILTIN_SOURCE_FILE.to_string(),
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

pub(super) fn ensure_builtin_element_templates(&self) {
let Some(element_templates) = &self.element_templates else {
return;
};

let mut templates = element_templates.borrow_mut();
if templates.is_empty() {
return;
}

// Raw text can also appear as dynamic element-slot content. Emitting the
// builtin template only when user ET templates exist keeps non-ET transforms
// free of template metadata while giving runtime a stable key for text slots.
if templates
.iter()
.any(|template| template.template_id == BUILTIN_RAW_TEXT_TEMPLATE_ID)
{
return;
}

templates.push(self.builtin_raw_text_template_asset());
}
}
Loading
Loading