From adc2e2a01adec1bbc4ac712ef552e8df4e2c6fd6 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Mon, 8 Dec 2025 22:23:49 -0800 Subject: [PATCH 1/7] github in javascript --- codepress-swc-plugin/src/lib.rs | 129 +++++++++++++++++--------------- package.json | 2 +- src/esbuild-plugin.ts | 28 +++---- src/index.ts | 18 +++-- 4 files changed, 94 insertions(+), 83 deletions(-) diff --git a/codepress-swc-plugin/src/lib.rs b/codepress-swc-plugin/src/lib.rs index f090520..35a6c70 100644 --- a/codepress-swc-plugin/src/lib.rs +++ b/codepress-swc-plugin/src/lib.rs @@ -262,6 +262,8 @@ pub struct CodePressTransform { use_js_metadata_map: bool, metadata_map: HashMap, inserted_metadata_map: bool, + // Track if config (repo/branch) has been injected to window.__CODEPRESS_CONFIG__ + inserted_config: bool, } /// Metadata entry for the JS-based map (window.__CODEPRESS_MAP__) @@ -477,6 +479,7 @@ impl CodePressTransform { use_js_metadata_map, metadata_map: HashMap::new(), inserted_metadata_map: false, + inserted_config: false, } } @@ -937,47 +940,9 @@ impl CodePressTransform { }) } - fn create_repo_attr(&self) -> Option { - self.repo_name.as_ref().map(|repo| { - JSXAttrOrSpread::JSXAttr(JSXAttr { - span: DUMMY_SP, - name: JSXAttrName::Ident(cp_ident_name("codepress-github-repo-name".into())), - value: Some(make_jsx_str_attr_value(repo.clone())), - }) - }) - } - - fn create_branch_attr(&self) -> Option { - self.branch_name.as_ref().map(|branch| { - JSXAttrOrSpread::JSXAttr(JSXAttr { - span: DUMMY_SP, - name: JSXAttrName::Ident(cp_ident_name("codepress-github-branch".into())), - value: Some(make_jsx_str_attr_value(branch.clone())), - }) - }) - } - - fn has_repo_attribute(&self, attrs: &[JSXAttrOrSpread]) -> bool { - attrs.iter().any(|attr| { - if let JSXAttrOrSpread::JSXAttr(jsx_attr) = attr { - if let JSXAttrName::Ident(ident) = &jsx_attr.name { - return ident.sym.as_ref() == "codepress-github-repo-name"; - } - } - false - }) - } - - fn has_branch_attribute(&self, attrs: &[JSXAttrOrSpread]) -> bool { - attrs.iter().any(|attr| { - if let JSXAttrOrSpread::JSXAttr(jsx_attr) = attr { - if let JSXAttrName::Ident(ident) = &jsx_attr.name { - return ident.sym.as_ref() == "codepress-github-branch"; - } - } - false - }) - } + // NOTE: create_repo_attr, create_branch_attr, has_repo_attribute, has_branch_attribute + // were removed - repo/branch info is now injected via window.__CODEPRESS_CONFIG__ + // in inject_config() instead of as HTML attributes. // ---------- binding collection & tracing ---------- @@ -2138,6 +2103,64 @@ impl CodePressTransform { self.inserted_metadata_map = true; } + /// Injects window.__CODEPRESS_CONFIG__ with repo and branch info. + /// This stores config in JS instead of as HTML attributes (which pollutes the DOM). + /// Only injected once per build (uses static flag to prevent duplicates across modules). + fn inject_config(&mut self, m: &mut Module) { + // Skip if already injected (either in this module or globally) + if self.inserted_config || GLOBAL_ATTRIBUTES_ADDED.load(Ordering::Relaxed) { + return; + } + + // Only inject if we have repo info + let repo = match &self.repo_name { + Some(r) => r.clone(), + None => return, + }; + + let branch = self.branch_name.clone().unwrap_or_else(|| "main".to_string()); + + // Build the config object: window.__CODEPRESS_CONFIG__ = { repo: "...", branch: "..." } + // Uses Object.assign to avoid overwriting if somehow multiple modules try to set it + let js = format!( + "try{{if(typeof window!=='undefined'){{window.__CODEPRESS_CONFIG__=Object.assign(window.__CODEPRESS_CONFIG__||{{}},{{repo:\"{}\",branch:\"{}\"}});}}}}catch(_){{}}", + repo.replace('\\', "\\\\").replace('"', "\\\""), + branch.replace('\\', "\\\\").replace('"', "\\\"") + ); + + let stmt = ModuleItem::Stmt(Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + callee: Callee::Expr(Box::new(Expr::New(NewExpr { + span: DUMMY_SP, + callee: Box::new(Expr::Ident(cp_ident("Function".into()))), + args: Some(vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: js.into(), + raw: None, + }))), + }]), + type_args: None, + #[cfg(not(feature = "compat_0_87"))] + ctxt: SyntaxContext::empty(), + }))), + args: vec![], + type_args: None, + #[cfg(not(feature = "compat_0_87"))] + ctxt: SyntaxContext::empty(), + })), + })); + + // Place AFTER directive prologue (e.g., "use client"; "use strict") + let insert_at = self.directive_insert_index(m); + m.body.insert(insert_at, stmt); + self.inserted_config = true; + GLOBAL_ATTRIBUTES_ADDED.store(true, Ordering::Relaxed); + } + /// Injects the CPRefreshProvider at app entry points (when use_js_metadata_map is true). /// This enables automatic HMR without users needing to manually add the provider. /// @@ -3240,6 +3263,8 @@ impl VisitMut for CodePressTransform { self.inject_graph_stmt(m); // Inject metadata map (if using JS-based metadata instead of DOM attributes) self.inject_metadata_map(m); + // Inject config (repo/branch) into window.__CODEPRESS_CONFIG__ (cleaner than DOM attributes) + self.inject_config(m); } fn visit_mut_import_decl(&mut self, n: &mut ImportDecl) { let _ = self.file_from_span(n.span); @@ -3686,26 +3711,8 @@ impl VisitMut for CodePressTransform { } } - // Root repo/branch once - if self.repo_name.is_some() && !GLOBAL_ATTRIBUTES_ADDED.load(Ordering::Relaxed) { - let element_name = match &node.opening.name { - JSXElementName::Ident(ident) => ident.sym.as_ref(), - _ => "", - }; - if matches!(element_name, "html" | "body" | "div") { - if !self.has_repo_attribute(&node.opening.attrs) { - if let Some(repo_attr) = self.create_repo_attr() { - node.opening.attrs.push(repo_attr); - } - } - if !self.has_branch_attribute(&node.opening.attrs) { - if let Some(branch_attr) = self.create_branch_attr() { - node.opening.attrs.push(branch_attr); - } - } - GLOBAL_ATTRIBUTES_ADDED.store(true, Ordering::Relaxed); - } - } + // NOTE: repo/branch info is now injected via window.__CODEPRESS_CONFIG__ in inject_config() + // instead of as HTML attributes, to avoid polluting the DOM. // Host vs custom let is_host = matches!( diff --git a/package.json b/package.json index 175172a..6e39612 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@codepress/codepress-engine", - "version": "0.8.0", + "version": "0.9.0", "packageManager": "pnpm@10.22.0", "description": "CodePress engine - Babel and SWC plug-ins", "main": "./dist/index.js", diff --git a/src/esbuild-plugin.ts b/src/esbuild-plugin.ts index e354a97..7e6c621 100644 --- a/src/esbuild-plugin.ts +++ b/src/esbuild-plugin.ts @@ -35,8 +35,9 @@ function xorEncodePath(input: string): string { /** * Inject codepress-data-fp attributes into JSX opening tags + * Note: repo/branch config is now injected via window.__CODEPRESS_CONFIG__ at module level */ -function injectJSXAttributes(source: string, encoded: string, repoName?: string, branchName?: string): string { +function injectJSXAttributes(source: string, encoded: string): string { const lines = source.split('\n'); const output: string[] = []; let lineNum = 0; @@ -60,20 +61,10 @@ function injectJSXAttributes(source: string, encoded: string, repoName?: string, const modifiedLine = line.replace( /(^|\s+|[\s{(>]|return\s+|=\s*|:\s*|\?\s*)<([A-Z][\w.]*|[a-z]+)([\s\/>])/g, (match, before, tagName, after) => { - // Build attributes + // Build attributes - only codepress-data-fp, repo/branch are injected via window.__CODEPRESS_CONFIG__ const attrs: string[] = []; attrs.push(`codepress-data-fp="${encoded}:${lineNum}-${lineNum}"`); - // Add repo/branch info to container elements (divs, sections, etc.) - if (/^[a-z]/.test(tagName)) { - if (repoName) { - attrs.push(`codepress-github-repo-name="${repoName}"`); - } - if (branchName) { - attrs.push(`codepress-github-branch="${branchName}"`); - } - } - return `${before}<${tagName} ${attrs.join(' ')}${after}`; } ); @@ -112,10 +103,19 @@ export function createCodePressPlugin(options: CodePressPluginOptions = {}): Plu // Inject JSX attributes (codepress-data-fp) // HMR is handled by a root-level CPRefreshProvider, not per-component wrapping - const transformed = injectJSXAttributes(source, encoded, repo_name, branch_name); + const transformed = injectJSXAttributes(source, encoded); + + // Inject config (repo/branch) into window.__CODEPRESS_CONFIG__ at module level + // This keeps the DOM clean instead of polluting HTML with attributes + let configPrefix = ''; + if (repo_name) { + const escapedRepo = repo_name.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const escapedBranch = (branch_name || 'main').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + configPrefix = `try{if(typeof window!=='undefined'){window.__CODEPRESS_CONFIG__=Object.assign(window.__CODEPRESS_CONFIG__||{},{repo:"${escapedRepo}",branch:"${escapedBranch}"});}}catch(_){}\n`; + } return { - contents: transformed, + contents: configPrefix + transformed, loader: 'tsx', }; } catch (err) { diff --git a/src/index.ts b/src/index.ts index 5c7bd26..61241d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -284,15 +284,19 @@ try { } } - // Inject repo/branch script (existing code) + // Inject repo/branch config into window.__CODEPRESS_CONFIG__ (cleaner than DOM attributes) if (!globalAttributesAdded && repoName && state.file.encodedPath) { + const escapedRepo = repoName.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const escapedBranch = (branch || 'main').replace(/\\/g, '\\\\').replace(/"/g, '\\"'); const scriptCode = ` -(function() { - if (typeof document !== 'undefined' && document.body && !document.body.hasAttribute('${REPO_ATTRIBUTE_NAME}')) { - document.body.setAttribute('${REPO_ATTRIBUTE_NAME}', '${repoName}'); - ${branch ? `document.body.setAttribute('${BRANCH_ATTRIBUTE_NAME}', '${branch}');` : ''} +try { + if (typeof window !== 'undefined') { + window.__CODEPRESS_CONFIG__ = Object.assign(window.__CODEPRESS_CONFIG__ || {}, { + repo: "${escapedRepo}", + branch: "${escapedBranch}" + }); } -})(); +} catch (_) {} `; const scriptAst = babel.template.ast(scriptCode); @@ -304,7 +308,7 @@ try { globalAttributesAdded = true; console.log( - `\x1b[32m✓ Injected repo/branch + graph in ${path.basename( + `\x1b[32m✓ Injected config + graph in ${path.basename( state.file.opts.filename ?? "unknown" )}\x1b[0m` ); From 75820386def2442e760ce6535a2e56c2d41d69fa Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 9 Dec 2025 00:48:01 -0800 Subject: [PATCH 2/7] inject CPProvider differently --- codepress-swc-plugin/src/lib.rs | 347 +++++++++++++++++++++----------- 1 file changed, 227 insertions(+), 120 deletions(-) diff --git a/codepress-swc-plugin/src/lib.rs b/codepress-swc-plugin/src/lib.rs index 35a6c70..e40605b 100644 --- a/codepress-swc-plugin/src/lib.rs +++ b/codepress-swc-plugin/src/lib.rs @@ -3881,154 +3881,261 @@ pub fn process_transform( } // ----------------------------------------------------------------------------- -// Pass 3: Wrap default export JSX with __CPRefreshProvider at entry points +// Pass 3: Wrap default export component with __CPRefreshProvider at entry points // ----------------------------------------------------------------------------- +// +// Instead of trying to find and wrap return statements (which has many edge cases), +// we wrap the entire default export component from the outside: +// +// Before: +// export default function App({ Component, pageProps }) { +// const getLayout = Component.getLayout ?? ((page) => page); +// return getLayout(); +// } +// +// After: +// function __CP_OriginalApp({ Component, pageProps }) { +// const getLayout = Component.getLayout ?? ((page) => page); +// return getLayout(); +// } +// export default function App(__CP_props) { +// return <__CPRefreshProvider><__CP_OriginalApp {...__CP_props} />; +// } +// +// This approach: +// - Doesn't need to understand component internals +// - Works with getLayout, fragments, conditionals, etc. +// - Has no edge cases around return statement patterns struct RefreshProviderWrapper; -impl RefreshProviderWrapper { - /// Wrap a JSX element with <__CPRefreshProvider>... - fn wrap_with_refresh_provider(jsx: JSXElement) -> JSXElement { - let provider_name = JSXElementName::Ident(cp_ident("__CPRefreshProvider".into()).into()); - JSXElement { - span: DUMMY_SP, - opening: JSXOpeningElement { - name: provider_name.clone(), - attrs: vec![], - self_closing: false, - type_args: None, - span: DUMMY_SP, - }, - children: vec![JSXElementChild::JSXElement(Box::new(jsx))], - closing: Some(JSXClosingElement { - span: DUMMY_SP, - name: provider_name, - }), - } - } - - /// Check if this JSX element is already wrapped with __CPRefreshProvider - fn is_already_wrapped(jsx: &JSXElement) -> bool { - match &jsx.opening.name { - JSXElementName::Ident(id) => id.sym.as_ref() == "__CPRefreshProvider", - _ => false, - } - } -} - impl VisitMut for RefreshProviderWrapper { fn visit_mut_module(&mut self, m: &mut Module) { - // Find the default export and wrap its JSX return - for item in &mut m.body { + let mut new_items: Vec = Vec::new(); + let mut found_default_export = false; + + for item in m.body.drain(..) { match item { - // export default function X() { ... } + // export default function App(...) { ... } ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl { + span, decl: DefaultDecl::Fn(fn_expr), - .. })) => { - if let Some(body) = &mut fn_expr.function.body { - self.wrap_jsx_returns(body); - } + found_default_export = true; + + // Extract the original function as __CP_OriginalApp + let original_fn = ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + ident: cp_ident("__CP_OriginalApp".into()), + declare: false, + function: fn_expr.function, + }))); + new_items.push(original_fn); + + // Create wrapper: export default function App(__CP_props) { + // return <__CPRefreshProvider><__CP_OriginalApp {...__CP_props} />; + // } + let wrapper = Self::create_wrapper_default_export(span); + new_items.push(wrapper); } - // export default () => ... + + // export default () => { ... } or export default function(...) { ... } ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr { + span, expr, - .. })) => { - match &mut **expr { + match *expr { + // Arrow function: export default () => { ... } Expr::Arrow(arrow) => { - match &mut *arrow.body { - BlockStmtOrExpr::BlockStmt(body) => { - self.wrap_jsx_returns(body); - } - BlockStmtOrExpr::Expr(expr) => { - // Arrow with implicit return: () => - if let Expr::JSXElement(jsx) = &**expr { - if !Self::is_already_wrapped(jsx) { - let wrapped = Self::wrap_with_refresh_provider(*jsx.clone()); - **expr = Expr::JSXElement(Box::new(wrapped)); - } - } - if let Expr::Paren(paren) = &**expr { - if let Expr::JSXElement(jsx) = &*paren.expr { - if !Self::is_already_wrapped(jsx) { - let wrapped = Self::wrap_with_refresh_provider(*jsx.clone()); - **expr = Expr::Paren(ParenExpr { - span: paren.span, - expr: Box::new(Expr::JSXElement(Box::new(wrapped))), - }); - } - } - } - } - } + found_default_export = true; + + // Create: const __CP_OriginalApp = () => { ... } + let original_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Const, + declare: false, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: cp_ident("__CP_OriginalApp".into()), + type_ann: None, + }), + init: Some(Box::new(Expr::Arrow(arrow))), + definite: false, + }], + #[cfg(feature = "compat_0_87")] + ctxt: swc_core::common::SyntaxContext::empty(), + #[cfg(not(feature = "compat_0_87"))] + ctxt: SyntaxContext::empty(), + })))); + new_items.push(original_decl); + + // Create wrapper export + let wrapper = Self::create_wrapper_default_export(span); + new_items.push(wrapper); } + + // Anonymous function: export default function(...) { ... } Expr::Fn(fn_expr) => { - if let Some(body) = &mut fn_expr.function.body { - self.wrap_jsx_returns(body); - } + found_default_export = true; + + // Create: function __CP_OriginalApp(...) { ... } + let original_fn = ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + ident: cp_ident("__CP_OriginalApp".into()), + declare: false, + function: fn_expr.function, + }))); + new_items.push(original_fn); + + // Create wrapper export + let wrapper = Self::create_wrapper_default_export(span); + new_items.push(wrapper); + } + + // Identifier: export default App (where App is defined elsewhere) + Expr::Ident(ident) => { + found_default_export = true; + + // Create: const __CP_OriginalApp = App; + let original_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { + span: DUMMY_SP, + kind: VarDeclKind::Const, + declare: false, + decls: vec![VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(BindingIdent { + id: cp_ident("__CP_OriginalApp".into()), + type_ann: None, + }), + init: Some(Box::new(Expr::Ident(ident))), + definite: false, + }], + #[cfg(feature = "compat_0_87")] + ctxt: swc_core::common::SyntaxContext::empty(), + #[cfg(not(feature = "compat_0_87"))] + ctxt: SyntaxContext::empty(), + })))); + new_items.push(original_decl); + + // Create wrapper export + let wrapper = Self::create_wrapper_default_export(span); + new_items.push(wrapper); + } + + // Other expressions - keep as-is (shouldn't happen for _app) + other => { + new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr( + ExportDefaultExpr { + span, + expr: Box::new(other), + }, + ))); } - _ => {} } } - _ => {} + + // Keep all other items as-is + other => { + new_items.push(other); + } } } + + // Only proceed if we found and wrapped a default export + if !found_default_export { + // Restore items if nothing was wrapped + m.body = new_items; + return; + } + + m.body = new_items; } } impl RefreshProviderWrapper { - /// Find return statements with JSX and wrap them - fn wrap_jsx_returns(&mut self, body: &mut BlockStmt) { - for stmt in &mut body.stmts { - self.visit_mut_stmt(stmt); - } - } + /// Create the wrapper default export: + /// export default function App(__CP_props) { + /// return <__CPRefreshProvider><__CP_OriginalApp {...__CP_props} />; + /// } + fn create_wrapper_default_export(span: swc_core::common::Span) -> ModuleItem { + // Build: <__CP_OriginalApp {...__CP_props} /> + let original_component = JSXElement { + span: DUMMY_SP, + opening: JSXOpeningElement { + name: JSXElementName::Ident(cp_ident("__CP_OriginalApp".into()).into()), + attrs: vec![JSXAttrOrSpread::SpreadElement(SpreadElement { + dot3_token: DUMMY_SP, + expr: Box::new(Expr::Ident(cp_ident("__CP_props".into()))), + })], + self_closing: true, + type_args: None, + span: DUMMY_SP, + }, + children: vec![], + closing: None, + }; - fn visit_mut_stmt(&mut self, stmt: &mut Stmt) { - match stmt { - Stmt::Return(ret) => { - if let Some(arg) = &mut ret.arg { - self.wrap_jsx_expr(arg); - } - } - Stmt::If(if_stmt) => { - self.visit_mut_stmt(&mut if_stmt.cons); - if let Some(alt) = &mut if_stmt.alt { - self.visit_mut_stmt(alt); - } - } - Stmt::Block(block) => { - for s in &mut block.stmts { - self.visit_mut_stmt(s); - } - } - _ => {} - } - } + // Build: <__CPRefreshProvider><__CP_OriginalApp {...__CP_props} /> + let provider_name = JSXElementName::Ident(cp_ident("__CPRefreshProvider".into()).into()); + let wrapped_jsx = JSXElement { + span: DUMMY_SP, + opening: JSXOpeningElement { + name: provider_name.clone(), + attrs: vec![], + self_closing: false, + type_args: None, + span: DUMMY_SP, + }, + children: vec![JSXElementChild::JSXElement(Box::new(original_component))], + closing: Some(JSXClosingElement { + span: DUMMY_SP, + name: provider_name, + }), + }; - fn wrap_jsx_expr(&mut self, expr: &mut Box) { - match &mut **expr { - Expr::JSXElement(jsx) => { - if !Self::is_already_wrapped(jsx) { - let wrapped = Self::wrap_with_refresh_provider(*jsx.clone()); - **expr = Expr::JSXElement(Box::new(wrapped)); - } - } - Expr::Paren(paren) => { - if let Expr::JSXElement(jsx) = &*paren.expr { - if !Self::is_already_wrapped(jsx) { - let wrapped = Self::wrap_with_refresh_provider(*jsx.clone()); - paren.expr = Box::new(Expr::JSXElement(Box::new(wrapped))); - } - } - } - Expr::Cond(cond) => { - self.wrap_jsx_expr(&mut cond.cons); - self.wrap_jsx_expr(&mut cond.alt); - } - _ => {} - } + // Build: return <__CPRefreshProvider>...; + let return_stmt = Stmt::Return(ReturnStmt { + span: DUMMY_SP, + arg: Some(Box::new(Expr::JSXElement(Box::new(wrapped_jsx)))), + }); + + // Build: function App(__CP_props) { return ...; } + let wrapper_fn = FnExpr { + ident: Some(cp_ident("App".into())), + function: Box::new(Function { + params: vec![Param { + span: DUMMY_SP, + decorators: vec![], + pat: Pat::Ident(BindingIdent { + id: cp_ident("__CP_props".into()), + type_ann: None, + }), + }], + decorators: vec![], + span: DUMMY_SP, + body: Some(BlockStmt { + span: DUMMY_SP, + stmts: vec![return_stmt], + #[cfg(feature = "compat_0_87")] + ctxt: swc_core::common::SyntaxContext::empty(), + #[cfg(not(feature = "compat_0_87"))] + ctxt: SyntaxContext::empty(), + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + #[cfg(feature = "compat_0_87")] + ctxt: swc_core::common::SyntaxContext::empty(), + #[cfg(not(feature = "compat_0_87"))] + ctxt: SyntaxContext::empty(), + }), + }; + + // Build: export default function App(__CP_props) { ... } + ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl { + span, + decl: DefaultDecl::Fn(wrapper_fn), + })) } } From d9ad6ef88a7b7a5a3766968f8fd7c58229a56096 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 9 Dec 2025 01:02:20 -0800 Subject: [PATCH 3/7] fix build --- codepress-swc-plugin/src/lib.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/codepress-swc-plugin/src/lib.rs b/codepress-swc-plugin/src/lib.rs index e40605b..5b34ecd 100644 --- a/codepress-swc-plugin/src/lib.rs +++ b/codepress-swc-plugin/src/lib.rs @@ -3962,8 +3962,7 @@ impl VisitMut for RefreshProviderWrapper { init: Some(Box::new(Expr::Arrow(arrow))), definite: false, }], - #[cfg(feature = "compat_0_87")] - ctxt: swc_core::common::SyntaxContext::empty(), + // ctxt field only exists in newer SWC versions (not compat_0_87) #[cfg(not(feature = "compat_0_87"))] ctxt: SyntaxContext::empty(), })))); @@ -4009,8 +4008,7 @@ impl VisitMut for RefreshProviderWrapper { init: Some(Box::new(Expr::Ident(ident))), definite: false, }], - #[cfg(feature = "compat_0_87")] - ctxt: swc_core::common::SyntaxContext::empty(), + // ctxt field only exists in newer SWC versions (not compat_0_87) #[cfg(not(feature = "compat_0_87"))] ctxt: SyntaxContext::empty(), })))); @@ -4115,8 +4113,7 @@ impl RefreshProviderWrapper { body: Some(BlockStmt { span: DUMMY_SP, stmts: vec![return_stmt], - #[cfg(feature = "compat_0_87")] - ctxt: swc_core::common::SyntaxContext::empty(), + // ctxt field only exists in newer SWC versions (not compat_0_87) #[cfg(not(feature = "compat_0_87"))] ctxt: SyntaxContext::empty(), }), @@ -4124,8 +4121,7 @@ impl RefreshProviderWrapper { is_async: false, type_params: None, return_type: None, - #[cfg(feature = "compat_0_87")] - ctxt: swc_core::common::SyntaxContext::empty(), + // ctxt field only exists in newer SWC versions (not compat_0_87) #[cfg(not(feature = "compat_0_87"))] ctxt: SyntaxContext::empty(), }), From 8c6aa873b1f93339b930d4ba0857122bba93a4a2 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 9 Dec 2025 17:45:53 -0800 Subject: [PATCH 4/7] updating import build --- codepress-swc-plugin/src/lib.rs | 44 ++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/codepress-swc-plugin/src/lib.rs b/codepress-swc-plugin/src/lib.rs index 5b34ecd..c533fcb 100644 --- a/codepress-swc-plugin/src/lib.rs +++ b/codepress-swc-plugin/src/lib.rs @@ -3923,6 +3923,9 @@ impl VisitMut for RefreshProviderWrapper { })) => { found_default_export = true; + // Capture the original function's identifier (if named) to preserve SyntaxContext + let original_ident = fn_expr.ident.clone(); + // Extract the original function as __CP_OriginalApp let original_fn = ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident: cp_ident("__CP_OriginalApp".into()), @@ -3934,7 +3937,8 @@ impl VisitMut for RefreshProviderWrapper { // Create wrapper: export default function App(__CP_props) { // return <__CPRefreshProvider><__CP_OriginalApp {...__CP_props} />; // } - let wrapper = Self::create_wrapper_default_export(span); + // Pass original_ident to preserve SyntaxContext for __CP_stamp references + let wrapper = Self::create_wrapper_default_export(span, original_ident); new_items.push(wrapper); } @@ -3968,8 +3972,8 @@ impl VisitMut for RefreshProviderWrapper { })))); new_items.push(original_decl); - // Create wrapper export - let wrapper = Self::create_wrapper_default_export(span); + // Create wrapper export (no original ident for arrow functions) + let wrapper = Self::create_wrapper_default_export(span, None); new_items.push(wrapper); } @@ -3977,6 +3981,9 @@ impl VisitMut for RefreshProviderWrapper { Expr::Fn(fn_expr) => { found_default_export = true; + // Capture the original function's identifier (if named) to preserve SyntaxContext + let original_ident = fn_expr.ident.clone(); + // Create: function __CP_OriginalApp(...) { ... } let original_fn = ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { ident: cp_ident("__CP_OriginalApp".into()), @@ -3985,8 +3992,8 @@ impl VisitMut for RefreshProviderWrapper { }))); new_items.push(original_fn); - // Create wrapper export - let wrapper = Self::create_wrapper_default_export(span); + // Create wrapper export (pass original_ident to preserve SyntaxContext) + let wrapper = Self::create_wrapper_default_export(span, original_ident); new_items.push(wrapper); } @@ -3994,6 +4001,9 @@ impl VisitMut for RefreshProviderWrapper { Expr::Ident(ident) => { found_default_export = true; + // Clone the ident to preserve its SyntaxContext for the wrapper + let original_ident = ident.clone(); + // Create: const __CP_OriginalApp = App; let original_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { span: DUMMY_SP, @@ -4014,8 +4024,8 @@ impl VisitMut for RefreshProviderWrapper { })))); new_items.push(original_decl); - // Create wrapper export - let wrapper = Self::create_wrapper_default_export(span); + // Create wrapper export (pass original_ident to preserve SyntaxContext) + let wrapper = Self::create_wrapper_default_export(span, Some(original_ident)); new_items.push(wrapper); } @@ -4054,7 +4064,10 @@ impl RefreshProviderWrapper { /// export default function App(__CP_props) { /// return <__CPRefreshProvider><__CP_OriginalApp {...__CP_props} />; /// } - fn create_wrapper_default_export(span: swc_core::common::Span) -> ModuleItem { + /// + /// If `original_ident` is provided, we reuse its SyntaxContext to ensure that + /// references like `__CP_stamp(App, ...)` (added by CodePressTransform) resolve correctly. + fn create_wrapper_default_export(span: swc_core::common::Span, original_ident: Option) -> ModuleItem { // Build: <__CP_OriginalApp {...__CP_props} /> let original_component = JSXElement { span: DUMMY_SP, @@ -4096,9 +4109,22 @@ impl RefreshProviderWrapper { arg: Some(Box::new(Expr::JSXElement(Box::new(wrapped_jsx)))), }); + // Use the original identifier's SyntaxContext if available, otherwise create a new one. + // This is critical because CodePressTransform adds __CP_stamp(App, ...) referencing + // the original App's SyntaxContext. If we create a new context, SWC's hygiene will + // rename our wrapper (e.g., to App1), leaving __CP_stamp referencing a non-existent binding. + let wrapper_ident = match original_ident { + Some(mut ident) => { + // Keep the same sym and ctxt, just update the span + ident.span = DUMMY_SP; + ident + } + None => cp_ident("App".into()), + }; + // Build: function App(__CP_props) { return ...; } let wrapper_fn = FnExpr { - ident: Some(cp_ident("App".into())), + ident: Some(wrapper_ident), function: Box::new(Function { params: vec![Param { span: DUMMY_SP, From 3c69871c784ad39ea924839546db121faf64207b Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 9 Dec 2025 21:28:35 -0800 Subject: [PATCH 5/7] update codepress review --- .github/workflows/codepress-review.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codepress-review.yml b/.github/workflows/codepress-review.yml index 1ddf82f..a138134 100644 --- a/.github/workflows/codepress-review.yml +++ b/.github/workflows/codepress-review.yml @@ -21,12 +21,11 @@ jobs: fetch-depth: 0 - name: CodePress Review - uses: quantfive/codepress-review@v3 + uses: quantfive/codepress-review@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} model_provider: "openai" - model_name: "gpt-5" + model_name: "gpt-5.1" openai_api_key: ${{ secrets.OPENAI_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} gemini_api_key: ${{ secrets.GEMINI_API_KEY }} - blocking_only: true From fa82205f035f7008d1ade2837ef918a4cec1de01 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 9 Dec 2025 21:34:32 -0800 Subject: [PATCH 6/7] updating lib --- codepress-swc-plugin/src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/codepress-swc-plugin/src/lib.rs b/codepress-swc-plugin/src/lib.rs index c533fcb..948c133 100644 --- a/codepress-swc-plugin/src/lib.rs +++ b/codepress-swc-plugin/src/lib.rs @@ -4001,9 +4001,6 @@ impl VisitMut for RefreshProviderWrapper { Expr::Ident(ident) => { found_default_export = true; - // Clone the ident to preserve its SyntaxContext for the wrapper - let original_ident = ident.clone(); - // Create: const __CP_OriginalApp = App; let original_decl = ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { span: DUMMY_SP, @@ -4024,8 +4021,13 @@ impl VisitMut for RefreshProviderWrapper { })))); new_items.push(original_decl); - // Create wrapper export (pass original_ident to preserve SyntaxContext) - let wrapper = Self::create_wrapper_default_export(span, Some(original_ident)); + // Create wrapper export. + // IMPORTANT: Do NOT pass the original ident here because the original + // `function App` declaration still exists in the module. Passing the + // original ident would create a duplicate binding. The __CP_stamp + // reference to the original App is still valid since the original + // function declaration remains. + let wrapper = Self::create_wrapper_default_export(span, None); new_items.push(wrapper); } From 2203617585b5dc89a6b182b7f3d4ef766ff05828 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 9 Dec 2025 22:37:49 -0800 Subject: [PATCH 7/7] meta tag now --- codepress-swc-plugin/src/lib.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/codepress-swc-plugin/src/lib.rs b/codepress-swc-plugin/src/lib.rs index 948c133..e9ccdfb 100644 --- a/codepress-swc-plugin/src/lib.rs +++ b/codepress-swc-plugin/src/lib.rs @@ -2122,10 +2122,13 @@ impl CodePressTransform { // Build the config object: window.__CODEPRESS_CONFIG__ = { repo: "...", branch: "..." } // Uses Object.assign to avoid overwriting if somehow multiple modules try to set it + // Also injects and tags + // for content script detection (content scripts run in isolated JS context) + let escaped_repo = repo.replace('\\', "\\\\").replace('"', "\\\""); + let escaped_branch = branch.replace('\\', "\\\\").replace('"', "\\\""); let js = format!( - "try{{if(typeof window!=='undefined'){{window.__CODEPRESS_CONFIG__=Object.assign(window.__CODEPRESS_CONFIG__||{{}},{{repo:\"{}\",branch:\"{}\"}});}}}}catch(_){{}}", - repo.replace('\\', "\\\\").replace('"', "\\\""), - branch.replace('\\', "\\\\").replace('"', "\\\"") + "try{{if(typeof window!=='undefined'){{window.__CODEPRESS_CONFIG__=Object.assign(window.__CODEPRESS_CONFIG__||{{}},{{repo:\"{}\",branch:\"{}\"}});}}if(typeof document!=='undefined'&&document.head&&!document.querySelector('meta[name=\"codepress-repo\"]')){{var m=document.createElement('meta');m.name='codepress-repo';m.content='{}';document.head.appendChild(m);var b=document.createElement('meta');b.name='codepress-branch';b.content='{}';document.head.appendChild(b);}}}}catch(_){{}}", + escaped_repo, escaped_branch, escaped_repo, escaped_branch ); let stmt = ModuleItem::Stmt(Stmt::Expr(ExprStmt {