@symbo.ls/brender 3.8.9 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/render.js CHANGED
@@ -15,7 +15,7 @@ let _funcqlPlugin = null
15
15
  const getFuncqlPlugin = async () => {
16
16
  if (_funcqlPlugin) return _funcqlPlugin
17
17
  try {
18
- const mod = await import('@domql/funcql')
18
+ const mod = await import('@symbo.ls/funcql')
19
19
  _funcqlPlugin = mod.funcqlPlugin
20
20
  return _funcqlPlugin
21
21
  } catch {
@@ -23,7 +23,7 @@ const getFuncqlPlugin = async () => {
23
23
  }
24
24
  }
25
25
  import { parseHTML } from 'linkedom'
26
- import createEmotionInstance from '@emotion/css/create-instance'
26
+ import { css, injectGlobal, reset as resetCss } from '@symbo.ls/css'
27
27
 
28
28
  // Lightweight SSR polyglot functions — resolve translations from context
29
29
  // without needing the full polyglot plugin runtime
@@ -244,7 +244,7 @@ const resolveDomqlPackage = (ws, pkg, ...subpath) => {
244
244
  if (ws.isMonorepo) {
245
245
  return resolve(ws.monorepoRoot, 'packages', 'domql', 'packages', pkg, ...subpath)
246
246
  }
247
- const pkgJson = tryRequireResolve(ws, `@domql/${pkg}/package.json`)
247
+ const pkgJson = tryRequireResolve(ws, `@symbo.ls/${pkg}/package.json`)
248
248
  if (pkgJson) return resolve(dirname(pkgJson), ...subpath)
249
249
  return null
250
250
  }
@@ -361,9 +361,9 @@ const bundleCreateDomql = async () => {
361
361
  }
362
362
  }
363
363
  })
364
- // Resolve @domql/* packages
364
+ // Resolve @symbo.ls/* packages
365
365
  build.onResolve({ filter: /^@domql\// }, args => {
366
- const pkg = args.path.replace('@domql/', '')
366
+ const pkg = args.path.replace('@symbo.ls/', '')
367
367
  if (ws.isMonorepo) {
368
368
  const src = resolve(ws.monorepoRoot, 'packages', 'domql', 'packages', pkg, 'src', 'index.js')
369
369
  if (existsSync(src)) return { path: src }
@@ -472,7 +472,7 @@ const bundleCreateDomql = async () => {
472
472
  let contents = readFileSync(args.path, 'utf8')
473
473
  contents = contents.replace(
474
474
  /import\s*\{\s*Link\s*\}\s*from\s*['"]smbls['"]/,
475
- `const Link = { tag: 'a', attr: { href: (el) => el.props?.href } }`
475
+ `const Link = { tag: 'a', attr: { href: (el) => el.href } }`
476
476
  )
477
477
  return { contents, loader: 'js' }
478
478
  })
@@ -545,9 +545,9 @@ const UIKIT_STUBS = {
545
545
  Link: {
546
546
  tag: 'a',
547
547
  attr: {
548
- href: (el) => el.props?.href,
549
- target: (el) => el.props?.target,
550
- rel: (el) => el.props?.rel
548
+ href: (el) => el.href,
549
+ target: (el) => el.target,
550
+ rel: (el) => el.rel
551
551
  }
552
552
  },
553
553
  A: { extends: 'Link' },
@@ -556,14 +556,14 @@ const UIKIT_STUBS = {
556
556
  tag: 'img',
557
557
  attr: {
558
558
  src: (el) => {
559
- let src = el.props?.src
559
+ let src = el.src
560
560
  if (typeof src === 'string' && src.includes('{{')) {
561
561
  src = el.call('replaceLiteralsWithObjectFields', src, el.state)
562
562
  }
563
563
  return src
564
564
  },
565
- alt: (el) => el.props?.alt,
566
- loading: (el) => el.props?.loading
565
+ alt: (el) => el.alt,
566
+ loading: (el) => el.loading
567
567
  }
568
568
  },
569
569
  Image: { extends: 'Img' },
@@ -769,15 +769,8 @@ export const render = async (data, options = {}) => {
769
769
  }
770
770
  }
771
771
 
772
- // Create SSR emotion instance with speedy: false.
773
- // In linkedom, emotion's insertRule() doesn't handle @media rules properly,
774
- // so responsive CSS is lost. Non-speedy mode uses text nodes instead,
775
- // which preserves @media rules in cache.inserted as strings.
776
- const ssrEmotion = createEmotionInstance({
777
- key: 'smbls',
778
- container: document.head,
779
- speedy: false
780
- })
772
+ // Reset the atomic CSS engine for this render pass
773
+ resetCss()
781
774
 
782
775
  const ctx = {
783
776
  state: baseState,
@@ -796,6 +789,7 @@ export const render = async (data, options = {}) => {
796
789
  methods: data.methods || {},
797
790
  designSystem: structuredCloneDeep(data.designSystem || {}),
798
791
  files: data.files || {},
792
+ sharedLibraries: data.sharedLibraries || [],
799
793
  ...config,
800
794
  // Override polyglot with SSR-enriched version
801
795
  ...(polyglotConfig ? { polyglot: polyglotConfig } : {}),
@@ -803,8 +797,7 @@ export const render = async (data, options = {}) => {
803
797
  document,
804
798
  window,
805
799
  parent: { node: body },
806
- // Use SSR emotion instance (non-speedy) for proper @media rule extraction
807
- initOptions: { emotion: ssrEmotion },
800
+ initOptions: {},
808
801
  // Disable sourcemap tracking in SSR — it causes stack overflows
809
802
  // when state contains large data arrays (articles, events, etc.)
810
803
  domqlOptions: { sourcemap: false },
@@ -846,47 +839,19 @@ export const render = async (data, options = {}) => {
846
839
  // (e.g. detail page titles from fetched data) can be resolved
847
840
  const metadata = extractMetadata(data, route, element, element?.state)
848
841
 
849
- // Extract emotion-generated CSS
850
- // Emotion uses insertRule() (CSSOM) which doesn't populate textContent in linkedom.
851
- // We extract from: (1) emotion cache.inserted, (2) CSSOM sheet rules, (3) textContent fallback.
842
+ // Extract CSS from style tags in virtual head (atomic CSS engine writes here)
852
843
  const emotionCSS = []
853
- const emotionInstance = ctx.emotion || (element && element.context && element.context.emotion)
854
- if (emotionInstance && emotionInstance.cache) {
855
- const cache = emotionInstance.cache
856
- // cache.inserted: hash CSS string (or true if inserted via CSSOM)
857
- if (cache.inserted) {
858
- for (const key in cache.inserted) {
859
- const rule = cache.inserted[key]
860
- if (typeof rule === 'string' && rule) emotionCSS.push(rule)
861
- }
862
- }
863
- // Extract from CSSOM sheet rules (covers insertRule-based insertion)
864
- if (cache.sheet && cache.sheet.tags) {
865
- for (const tag of cache.sheet.tags) {
866
- if (tag.sheet && tag.sheet.cssRules) {
867
- for (const rule of tag.sheet.cssRules) {
868
- if (rule.cssText) emotionCSS.push(rule.cssText)
869
- }
844
+ const head = document.head || document.querySelector('head')
845
+ if (head) {
846
+ for (const style of head.querySelectorAll('style')) {
847
+ if (style.sheet && style.sheet.cssRules) {
848
+ for (const rule of style.sheet.cssRules) {
849
+ if (rule.cssText) emotionCSS.push(rule.cssText)
870
850
  }
871
851
  }
872
- }
873
- }
874
- // Fallback: scan all style tags in virtual head
875
- if (!emotionCSS.length) {
876
- const head = document.head || document.querySelector('head')
877
- if (head) {
878
- for (const style of head.querySelectorAll('style')) {
879
- // Try CSSOM rules first
880
- if (style.sheet && style.sheet.cssRules) {
881
- for (const rule of style.sheet.cssRules) {
882
- if (rule.cssText) emotionCSS.push(rule.cssText)
883
- }
884
- }
885
- // Fallback to textContent
886
- if (!emotionCSS.length) {
887
- const content = style.textContent || ''
888
- if (content) emotionCSS.push(content)
889
- }
852
+ if (!emotionCSS.length) {
853
+ const content = style.textContent || ''
854
+ if (content) emotionCSS.push(content)
890
855
  }
891
856
  }
892
857
  }
@@ -928,8 +893,8 @@ export const renderElement = async (elementDef, options = {}) => {
928
893
  const { window, document } = createEnv()
929
894
  const body = document.body
930
895
 
931
- const { create } = await import('@domql/element')
932
- const domqlUtils = await import('@domql/utils')
896
+ const { create } = await import('@symbo.ls/element')
897
+ const domqlUtils = await import('@symbo.ls/utils')
933
898
 
934
899
  // Merge minimal uikit stubs so DOMQL resolves extends chains
935
900
  // (e.g. extends: 'Link' → tag: 'a', extends: 'Flex' → display: flex)
@@ -993,6 +958,37 @@ const fixSvgContent = (html) => {
993
958
  */
994
959
  let _cachedGlobalCSS = null
995
960
 
961
+ // Frank serializes a project's `config.js` exports at the TOP LEVEL of the
962
+ // data payload (not under `data.config`), so `globalTheme` / `themeStorageKey`
963
+ // / `useReset` / etc. live alongside `components` / `pages` / `designSystem`.
964
+ // Both renderRoute and renderPage used to call `generateGlobalCSS(ds,
965
+ // data.config || data.settings)` which resolved to undefined for any project
966
+ // pushed through frank — every config flag silently dropped, including
967
+ // `globalTheme: 'light'`. The hardcoded `globalTheme: 'auto'` default in
968
+ // generateGlobalCSS then won, prod always rendered in matchMedia-detected
969
+ // theme regardless of what the project declared. Helper picks the config
970
+ // flags from `data` whichever shape they arrive in.
971
+ const SCRATCH_CONFIG_FLAGS = [
972
+ 'globalTheme', 'themeStorageKey', 'themeRoot',
973
+ 'useReset', 'useVariable', 'useFontImport', 'useIconSprite', 'useSvgSprite',
974
+ 'useDocumentTheme', 'useDefaultConfig', 'useDefaultIcons',
975
+ 'useThemeSuffixedVars', 'verbose', 'semanticIcons',
976
+ ]
977
+ const pickProjectConfig = (data) => {
978
+ if (!data || typeof data !== 'object') return null
979
+ if (data.config && typeof data.config === 'object') return data.config
980
+ const out = {}
981
+ let any = false
982
+ for (const flag of SCRATCH_CONFIG_FLAGS) {
983
+ if (Object.prototype.hasOwnProperty.call(data, flag)) {
984
+ out[flag] = data[flag]
985
+ any = true
986
+ }
987
+ }
988
+ if (any) return out
989
+ return data.settings || null
990
+ }
991
+
996
992
  const generateGlobalCSS = async (ds, config) => {
997
993
  if (_cachedGlobalCSS) return _cachedGlobalCSS
998
994
 
@@ -1058,7 +1054,7 @@ const generateGlobalCSS = async (ds, config) => {
1058
1054
  // Detect workspace layout (monorepo vs npm install)
1059
1055
  const ws = detectWorkspace()
1060
1056
 
1061
- // Workspace resolve plugin: maps @symbo.ls/* and @domql/* to source paths
1057
+ // Workspace resolve plugin: maps @symbo.ls/* and @symbo.ls/* to source paths
1062
1058
  const workspacePlugin = {
1063
1059
  name: 'workspace-resolve',
1064
1060
  setup (build) {
@@ -1085,7 +1081,7 @@ const generateGlobalCSS = async (ds, config) => {
1085
1081
  }
1086
1082
  })
1087
1083
  build.onResolve({ filter: /^@domql\// }, args => {
1088
- const pkg = args.path.replace('@domql/', '')
1084
+ const pkg = args.path.replace('@symbo.ls/', '')
1089
1085
  if (ws.isMonorepo) {
1090
1086
  const src = resolve(ws.monorepoRoot, 'packages', 'domql', 'packages', pkg, 'src', 'index.js')
1091
1087
  if (existsSync(src)) return { path: src }
@@ -1232,7 +1228,7 @@ export const renderRoute = async (data, options = {}) => {
1232
1228
  if (!result) return null
1233
1229
 
1234
1230
  const ds = data.designSystem || {}
1235
- const globalCSS = await generateGlobalCSS(ds, data.config || data.settings)
1231
+ const globalCSS = await generateGlobalCSS(ds, pickProjectConfig(data))
1236
1232
 
1237
1233
  // Extract prefetched state and language for metadata resolution
1238
1234
  let prefetchedState = null
@@ -1329,7 +1325,7 @@ export const renderPage = async (data, route = '/', options = {}) => {
1329
1325
 
1330
1326
  // Generate global CSS (variables, reset, keyframes) via scratch pipeline
1331
1327
  const ds = data.designSystem || {}
1332
- const globalCSS = await generateGlobalCSS(ds, data.config || data.settings)
1328
+ const globalCSS = await generateGlobalCSS(ds, pickProjectConfig(data))
1333
1329
 
1334
1330
  // Generate font links from design system
1335
1331
  const fontLinks = generateFontLinks(ds)
@@ -1754,33 +1750,30 @@ const getExtendsCSS = (el) => {
1754
1750
  * Uses fallback mock state if element state is incomplete.
1755
1751
  */
1756
1752
  const resolveElementProps = (el) => {
1757
- const { props } = el
1758
- if (!props) return props
1759
1753
  let resolved
1760
- for (const key in props) {
1761
- if (typeof props[key] !== 'function') continue
1754
+ for (const key in el) {
1755
+ if (typeof el[key] !== 'function') continue
1762
1756
  // Skip non-CSS props and component children
1763
1757
  if (NON_CSS_PROPS.has(key)) continue
1764
1758
  if (key.charCodeAt(0) >= 65 && key.charCodeAt(0) <= 90) continue
1765
1759
  if (key.startsWith('on')) continue
1766
- if (!resolved) resolved = { ...props }
1760
+ if (key.startsWith('__')) continue
1761
+ if (!resolved) resolved = {}
1767
1762
  let result
1768
1763
  try {
1769
- result = props[key](el, el.state || {})
1764
+ result = el[key](el, el.state || {})
1770
1765
  } catch {
1771
1766
  // State prototype chain may be incomplete in SSR — try with mock
1772
1767
  try {
1773
1768
  const mockState = { root: {}, ...(el.state || {}) }
1774
- result = props[key](el, mockState)
1769
+ result = el[key](el, mockState)
1775
1770
  } catch { /* skip prop */ }
1776
1771
  }
1777
1772
  if (result !== undefined && result !== null && result !== false) {
1778
1773
  resolved[key] = result
1779
- } else {
1780
- delete resolved[key]
1781
1774
  }
1782
1775
  }
1783
- return resolved || props
1776
+ return resolved || el
1784
1777
  }
1785
1778
 
1786
1779
  const extractCSS = (element, ds) => {