Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@
---

---
Comment thread
Yradex marked this conversation as resolved.
59 changes: 59 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
90 changes: 90 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,96 @@ 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 export template ids for element template uiSourceMapRecords', async () => {
const result = await transformReactLynx('const node = <view />;', {
mode: 'test',
pluginName: '',
filename: 'test.js',
sourcemap: false,
cssScope: false,
elementTemplate: {
preserveJsx: false,
runtimePkg: '@lynx-js/react',
filename: 'test.js',
target: 'LEPUS',
enableUiSourceMap: true,
},
jsx: true,
directiveDCE: false,
defineDCE: false,
shake: false,
compat: true,
worklet: false,
refresh: false,
});

expect(result.uiSourceMapRecords).toHaveLength(1);
expect(result.uiSourceMapRecords[0]).toMatchObject({
filename: 'test.js',
templateId: expect.stringMatching(/^_et_/),
});
expect(result.uiSourceMapRecords[0].snapshotId).toBeUndefined();
});

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();
});
});

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_minifier", "ecma_transforms_typescript", "ecma_utils", "ecma_quote", "ecma_transforms_react", "ecma_transforms_optimization", "__visit", "__testing_transform"] }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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,64 @@
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__";

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: self.cfg.filename.clone(),
}
}
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