@dongmingzhixiu/vuexir-compiler 0.7.7 → 0.7.9

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.
@@ -109,6 +109,7 @@ declare function buildNavHost(bundleId: string, tabs: {
109
109
  mode?: string;
110
110
  }[], options?: {
111
111
  tabBar?: TabBarSpec;
112
+ nativeNavigationBar?: boolean;
112
113
  }): string;
113
114
  /**
114
115
  * 输入解析(R-PKG):CLI 通过 VRN_SAMPLES_ROOT 注入样例根(默认 <repo>/examples)。
@@ -11,6 +11,8 @@
11
11
  import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync } from 'node:fs';
12
12
  import { join, dirname, basename } from 'node:path';
13
13
  import { fileURLToPath } from 'node:url';
14
+ import { NodeTypes } from '@vue/compiler-core';
15
+ import { parse } from '@vue/compiler-sfc';
14
16
  import { loadProjectAssets, materializeProjectAssets } from './project-assets.js';
15
17
  import { extractCssVariables } from '../transform/css.js';
16
18
  import { loadProjectStyles } from './project-styles.js';
@@ -476,8 +478,16 @@ function dp(n) {
476
478
  function sp(n) {
477
479
  return `${n}.sp`;
478
480
  }
481
+ function kotlinStringContents(value) {
482
+ return value
483
+ .replace(/\\/g, '\\\\')
484
+ .replace(/"/g, '\\"')
485
+ .replace(/\$/g, () => '\\$')
486
+ .replace(/\n/g, '\\n')
487
+ .replace(/\r/g, '\\r');
488
+ }
479
489
  function kotlinStr(s) {
480
- return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
490
+ return `"${kotlinStringContents(s)}"`;
481
491
  }
482
492
  /** 条件表达式 → Kotlin(对位 iOS swiftCondExpr:=== / !== 归一 + 单引号字符串转双引号) */
483
493
  /** R114:文本绑定里引用的标识符若既不是页面状态也不是循环变量 → 视为未支持,回退空串 */
@@ -1381,7 +1391,7 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1381
1391
  const segs = p.bindParts
1382
1392
  .map((seg) => {
1383
1393
  if ('text' in seg)
1384
- return String(seg.text).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '${\'$\'}').replace(/\n/g, '\\n');
1394
+ return kotlinStringContents(String(seg.text));
1385
1395
  if ('bind' in seg)
1386
1396
  return '${' + safeBind(seg.bind, ctx, loop) + '}';
1387
1397
  if ('fieldOf' in seg)
@@ -1519,17 +1529,18 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
1519
1529
  }
1520
1530
  case 'image': {
1521
1531
  const source = p.src ?? '';
1532
+ const contentScale = p.objectFit === 'fill' ? 'FillBounds' : p.objectFit === 'contain' ? 'Fit' : 'Crop';
1522
1533
  const chain = modChain([...sizeModifiers(p, ctx), ...absM(p), ...clipModifiers(p), ...visualModifiers(p), ...paddingModifiers(p), ...alphaArr], pad + ' ');
1523
1534
  if (source.startsWith('asset://')) {
1524
1535
  const name = source.slice(8);
1525
- return pad + 'run { val imageContext = androidx.compose.ui.platform.LocalContext.current; val bitmap = remember { imageContext.assets.open(' + kotlinStr(name) + ').use { android.graphics.BitmapFactory.decodeStream(it) } }; if(bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' + (p.objectFit === 'contain' ? 'Fit' : 'Crop') + ', modifier = Modifier' + chain + ') }';
1536
+ return pad + 'run { val imageContext = androidx.compose.ui.platform.LocalContext.current; val bitmap = remember { imageContext.assets.open(' + kotlinStr(name) + ').use { android.graphics.BitmapFactory.decodeStream(it) } }; if(bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' + contentScale + ', modifier = Modifier' + chain + ') }';
1526
1537
  }
1527
1538
  if (/^data:image\/[^;]+;base64,/.test(source)) {
1528
1539
  const encoded = source.slice(source.indexOf(',') + 1);
1529
1540
  return pad + 'run {\n' + pad + ' val imageBytes = remember { android.util.Base64.decode(' + kotlinStr(encoded) + ', android.util.Base64.DEFAULT) }\n' +
1530
1541
  pad + ' val bitmap = remember { android.graphics.BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) }\n' +
1531
1542
  pad + ' if (bitmap != null) Image(bitmap = bitmap.asImageBitmap(), contentDescription = null, contentScale = androidx.compose.ui.layout.ContentScale.' +
1532
- (p.objectFit === 'contain' ? 'Fit' : 'Crop') + ', modifier = Modifier' + chain + ')\n' + pad + '}';
1543
+ contentScale + ', modifier = Modifier' + chain + ')\n' + pad + '}';
1533
1544
  }
1534
1545
  ctx.warnings.push(`图片 ${source.slice(0, 80)} 需要 data URI 资源映射,Android 未生成图片内容`);
1535
1546
  return pad + 'Box(modifier = Modifier' + chain + ')';
@@ -1755,10 +1766,10 @@ import androidx.compose.ui.unit.dp
1755
1766
  import androidx.compose.ui.unit.sp
1756
1767
  `;
1757
1768
  const TAB_ICONS = ['home.fill', 'book.fill', 'square.grid.2x2.fill', 'bell.fill', 'gearshape.fill', 'explore', 'work'];
1758
- /** tab 文案拆 2 字一行(对位 iOS 窄 tab 自动换行视觉) */
1769
+ /** 纯汉字 tab 保持两字一行,其他标题由系统按可用宽度换行。 */
1759
1770
  function tabLabel(title) {
1760
- const t = title.replace(/\s+/g, '');
1761
- return t.length > 2 ? t.match(/.{1,2}/g).join('\\n') : t;
1771
+ const compact = title.replace(/\s+/g, '');
1772
+ return /^\p{Script=Han}+$/u.test(compact) ? compact.match(/.{1,2}/gu).join('\n') : title;
1762
1773
  }
1763
1774
  /**
1764
1775
  * 工程根推导(R75):路由文件可能在 src/router/index.ts,`@/` 别名需要相对「工程根」解析。
@@ -1878,14 +1889,20 @@ function parseRouter(src, dir, routerFile) {
1878
1889
  meta,
1879
1890
  });
1880
1891
  }
1881
- const hasText = (file) => {
1892
+ const hasRenderableNode = (node) => {
1893
+ if (node.type === NodeTypes.ELEMENT) {
1894
+ return node.tag !== 'template' || node.children.some(hasRenderableNode);
1895
+ }
1896
+ return node.type === NodeTypes.INTERPOLATION || (node.type === NodeTypes.TEXT && node.content.trim() !== '');
1897
+ };
1898
+ const hasRenderableTemplate = (file) => {
1882
1899
  if (!file || !existsSync(file))
1883
1900
  return false;
1884
- const tpl = readFileSync(file, 'utf-8').match(/<template>([\s\S]*)<\/template>/)?.[1] ?? '';
1885
- return tpl.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().length > 0;
1901
+ const { descriptor } = parse(readFileSync(file, 'utf-8'), { filename: file });
1902
+ return descriptor.template?.ast?.children.some(hasRenderableNode) ?? false;
1886
1903
  };
1887
1904
  const tabs = routes
1888
- .filter((r) => r.meta.tab === 'true' && hasText(r.file))
1905
+ .filter((r) => r.meta.tab === 'true' && hasRenderableTemplate(r.file))
1889
1906
  .map((r, i) => ({
1890
1907
  id: r.name,
1891
1908
  title: r.meta.title ?? r.name,
@@ -1894,7 +1911,7 @@ function parseRouter(src, dir, routerFile) {
1894
1911
  path: r.path,
1895
1912
  }));
1896
1913
  const details = routes
1897
- .filter((r) => r.meta.tab !== 'true' && hasText(r.file))
1914
+ .filter((r) => r.meta.tab !== 'true' && hasRenderableTemplate(r.file))
1898
1915
  .map((r) => ({ id: r.name, title: r.meta.title ?? r.name, file: r.file, path: r.path }));
1899
1916
  if (tabs.length === 0) {
1900
1917
  warnings.push('router 未解析到任何 tab 路由(routes 需含 meta: { tab: true })');
@@ -2083,9 +2100,10 @@ function runTabs(appDir, opts = {}) {
2083
2100
  console.log(`[gen-android] - ${pageId}DetailView.kt · push「${d.title}」`);
2084
2101
  }
2085
2102
  // 3) AppNavHost.kt(router.ts → NavHost + NavigationBar)
2086
- let navHost = buildNavHost(bundleId, tabs, detailViews, { tabBar: opts.tabBar });
2087
- if (opts.nativeNavigationBar === false)
2088
- navHost = navHost.replace(/ topBar = \{[\s\S]*? \},\n bottomBar =/, ' bottomBar =');
2103
+ const navHost = buildNavHost(bundleId, tabs, detailViews, {
2104
+ tabBar: opts.tabBar,
2105
+ nativeNavigationBar: opts.nativeNavigationBar,
2106
+ });
2089
2107
  writeFileSync(join(pkgDir, 'AppNavHost.kt'), navHost);
2090
2108
  // P2:写 .vrn/manifest.json(工具链/模板版本 + 文件清单),供 vrn doctor 校验与增量重生成
2091
2109
  const metaDir = join(outDir, '.vrn');
@@ -2112,6 +2130,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2112
2130
  .map((t) => `composable("${t.id}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }
2113
2131
  composable("${t.path}") { TabScaffold("${t.id}", onSelectTab) { p -> ${toPascal(t.id)}Content(modifier = Modifier.padding(p)) } }`)
2114
2132
  .join('\n ');
2133
+ const showNavigationBar = options.nativeNavigationBar !== false;
2115
2134
  // M7 对位 iOS NavigationStack:详情页内层 Scaffold + TopAppBar(标题 + 返回箭头/backText、mode=transparent 透明背景)
2116
2135
  const detailRoutes = details
2117
2136
  .map((d) => {
@@ -2128,7 +2147,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2128
2147
  const barColor = d.mode === 'transparent' ? 'Color.Transparent' : d.bg ? colorExpr(d.bg) : 'InkBar';
2129
2148
  return `composable("${d.path}") {
2130
2149
  Scaffold(
2131
- containerColor = InkBg,
2150
+ containerColor = InkBg,${showNavigationBar ? `
2132
2151
  topBar = {
2133
2152
  TopAppBar(
2134
2153
  title = { Text(${kotlinStr(d.title)}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
@@ -2141,7 +2160,7 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2141
2160
  titleContentColor = Gold,
2142
2161
  ),
2143
2162
  )
2144
- },
2163
+ },` : ''}
2145
2164
  ) { innerPadding ->
2146
2165
  ${toPascal(d.id)}DetailContent(modifier = Modifier.padding(innerPadding))
2147
2166
  }
@@ -2152,8 +2171,8 @@ function buildNavHost(bundleId, tabs, details, options = {}) {
2152
2171
  .map((t) => `NavigationBarItem(
2153
2172
  selected = currentTab == "${t.id}",
2154
2173
  onClick = { onSelectTab("${t.id}") },
2155
- icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription = "${t.title}") },
2156
- label = { Text("${tabLabel(t.title)}", fontSize = 10.sp, textAlign = TextAlign.Center) },
2174
+ icon = { Icon(${ICON_MAP[t.icon] ?? 'Icons.Default.Home'}, contentDescription = ${kotlinStr(t.title)}) },
2175
+ label = { Text(${kotlinStr(tabLabel(t.title))}, fontSize = 10.sp, textAlign = TextAlign.Center) },
2157
2176
  colors = NavigationBarItemDefaults.colors(
2158
2177
  selectedIconColor = Gold,
2159
2178
  selectedTextColor = Gold,
@@ -2217,7 +2236,7 @@ import androidx.navigation.compose.*
2217
2236
  @Composable
2218
2237
  fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Composable (PaddingValues) -> Unit) {
2219
2238
  Scaffold(
2220
- containerColor = InkBg,
2239
+ containerColor = InkBg,${showNavigationBar ? `
2221
2240
  topBar = {
2222
2241
  TopAppBar(
2223
2242
  title = { Text(${tabTitleExpr}, fontSize = 17.sp, fontWeight = FontWeight.Medium) },
@@ -2226,7 +2245,7 @@ fun TabScaffold(currentTab: String, onSelectTab: (String) -> Unit, content: @Com
2226
2245
  titleContentColor = Gold,
2227
2246
  ),
2228
2247
  )
2229
- },
2248
+ },` : ''}
2230
2249
  bottomBar = {
2231
2250
  ${customBar || `NavigationBar(containerColor = InkBar) {\n ${barItems}\n }`}
2232
2251
  },
@@ -346,7 +346,7 @@ function gapInheritFor(child, parentGap, siblings) {
346
346
  return parentGap;
347
347
  }
348
348
  /* ============ 节点发射 ============ */
349
- function sizeModifiers(props, axis, ctx) {
349
+ function sizeModifiers(props, axis, ctx, fixedFrameAlignment) {
350
350
  const mods = [];
351
351
  const { width, height, flex } = props;
352
352
  if (props.widthDynamic)
@@ -356,11 +356,11 @@ function sizeModifiers(props, axis, ctx) {
356
356
  if (width === 'fill')
357
357
  mods.push(frameExpr('fillW'));
358
358
  else if (typeof width === 'number')
359
- mods.push(`.frame(width: ${width})`);
359
+ mods.push(`.frame(width: ${width}${fixedFrameAlignment})`);
360
360
  if (height === 'fill')
361
361
  mods.push('.frame(maxHeight: .infinity)');
362
362
  if (typeof height === 'number')
363
- mods.push(`.frame(height: ${height})`);
363
+ mods.push(`.frame(height: ${height}${fixedFrameAlignment})`);
364
364
  if (flex !== undefined) {
365
365
  // R-H3:flex 沿父容器主轴拉伸占满剩余空间;但父容器高度不确定时(auto 链)无剩余空间可分配,
366
366
  // SwiftUI 的 .infinity 会沿祖先链漂移到屏幕高度 → 降级为内容高并告警(与 CSS flex-grow 语义一致)
@@ -707,10 +707,14 @@ function emitNodeInner(node, ctx) {
707
707
  return pad(`VrnFractionWidth(fraction: max(0,min(1,${fraction}))) {`) + `\n${inner}\n${pad('}')}`;
708
708
  }
709
709
  const axis = ctx.parentAxis ?? 'column';
710
+ // 绝对定位偏移以固定容器的左上角为原点。SwiftUI 默认会居中放置偏移前的 ZStack。
711
+ const fixedFrameAlignment = node.type === 'group' && !props.flexWrap && props.gridColumns === undefined &&
712
+ node.children?.length && node.children.every((child) => child.props?.position === 'absolute')
713
+ ? ', alignment: .topLeading' : '';
710
714
  // box-sizing:border-box(CSS 默认):固定 height 应包含 padding。
711
715
  // 故 height 为数字且存在 padding 时,padding 置于 frame(height) 之前、
712
716
  // frame 限定总视觉高度(否则 padding 加在 frame 外 → 视觉高 = height + 2×padding)。
713
- const size = sizeModifiers(props, axis, ctx);
717
+ const size = sizeModifiers(props, axis, ctx, fixedFrameAlignment);
714
718
  let padMods = paddingModifiers(props);
715
719
  let boxMods = boxModifiers(props);
716
720
  if (node.type === 'image' && props.objectFit === 'cover' && !props.clip) {
@@ -723,7 +727,7 @@ function emitNodeInner(node, ctx) {
723
727
  boxMods = boxMods.filter((m) => !m.startsWith('.background(') && !m.startsWith('.cornerRadius(') && !m.startsWith('.overlay('));
724
728
  }
725
729
  const box = typeof props.height === 'number' && (padMods.length > 0 || capsule !== undefined)
726
- ? [...size.filter((m) => !m.startsWith('.frame(height: ')), ...(capsule !== undefined ? [capsule] : []), ...padMods, `.frame(height: ${props.height})`, ...boxMods]
730
+ ? [...size.filter((m) => !m.startsWith('.frame(height: ')), ...(capsule !== undefined ? [capsule] : []), ...padMods, `.frame(height: ${props.height}${fixedFrameAlignment})`, ...boxMods]
727
731
  : [...size, ...(capsule !== undefined ? [capsule] : []), ...padMods, ...boxMods];
728
732
  // M5 transform:scale / rotate / translate → 原生修饰符(视觉变换,不影响布局占位)
729
733
  const t = props.transform;
@@ -1288,7 +1292,9 @@ function emitNodeInner(node, ctx) {
1288
1292
  else {
1289
1293
  image = `Image(uiImage: UIImage(named: "${swiftStringEscape(source.replace(/^bundle:\/\//, ''))}") ?? UIImage()).resizable()`;
1290
1294
  }
1291
- image += props.objectFit === 'contain' ? '.scaledToFit()' : '.scaledToFill()';
1295
+ if (props.objectFit !== 'fill') {
1296
+ image += props.objectFit === 'contain' ? '.scaledToFit()' : '.scaledToFill()';
1297
+ }
1292
1298
  return pad(image) + box.map((m) => ' ' + m).join('');
1293
1299
  }
1294
1300
  case 'component': {
@@ -17,7 +17,7 @@ export type Justify = 'start' | 'center' | 'end' | 'spaceBetween' | 'spaceAround
17
17
  export type Align = 'start' | 'center' | 'end' | 'stretch';
18
18
  export type FontWeight = 'normal' | 'medium' | 'bold';
19
19
  export type TextAlign = 'left' | 'center' | 'right';
20
- export type ObjectFit = 'cover' | 'contain';
20
+ export type ObjectFit = 'cover' | 'contain' | 'fill';
21
21
  export interface IrProps {
22
22
  /** 尺寸 */
23
23
  width?: Size;
package/dist/ir/schema.js CHANGED
@@ -101,7 +101,7 @@ const PROPS_VALIDATORS = {
101
101
  lineHeight: (v) => typeof v === 'number' && v > 0,
102
102
  color: isColor,
103
103
  src: (v) => typeof v === 'string',
104
- objectFit: (v) => v === 'cover' || v === 'contain',
104
+ objectFit: (v) => v === 'cover' || v === 'contain' || v === 'fill',
105
105
  label: (v) => typeof v === 'string',
106
106
  onClick: (v) => typeof v === 'string',
107
107
  showsIndicators: (v) => typeof v === 'boolean',
@@ -122,7 +122,7 @@ function mapDeclInner(prop, value, warnings) {
122
122
  return []; // 变量声明由 token 解析器处理,不是未支持的视觉属性。
123
123
  switch (prop) {
124
124
  case 'object-fit':
125
- return v === 'contain' || v === 'cover' ? [['objectFit', v]] : null;
125
+ return v === 'contain' || v === 'cover' || v === 'fill' ? [['objectFit', v]] : null;
126
126
  case 'aspect-ratio': {
127
127
  const parts = v.split('/').map((part) => Number(part.trim()));
128
128
  const ratio = parts.length === 1 ? parts[0] : parts.length === 2 ? parts[0] / parts[1] : NaN;
@@ -2362,10 +2362,18 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2362
2362
  const inlineTags = new Set(['span', 'b', 'strong', 'i', 'em', 'small', 'u', 'code', 'br']);
2363
2363
  const content = node.children.filter((c) => c.type !== NodeTypes.COMMENT && !(c.type === NodeTypes.TEXT && !c.content.trim()));
2364
2364
  const readable = (n) => n.type === NodeTypes.INTERPOLATION || (n.type === NodeTypes.TEXT && !!n.content.trim()) || (n.type === NodeTypes.ELEMENT && n.children.some(readable));
2365
- const inlineConditional = content.some((c) => c.type === NodeTypes.ELEMENT && c.props.some((p) => p.type === NodeTypes.DIRECTIVE && ['if', 'else-if', 'else', 'show'].includes(p.name)));
2366
- if (inlineComponentsFlag && mapping.type === 'group' && !inlineConditional && props.flexDirection === undefined && props.gridColumns === undefined && content.some(readable) && content.every((c) => c.type === NodeTypes.TEXT || c.type === NodeTypes.INTERPOLATION || (c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))))
2365
+ // 行内分支或循环需要独立节点;合并为富文本会丢弃这些动态子树。
2366
+ const hasInlineStructure = (child) => {
2367
+ if (child.type !== NodeTypes.ELEMENT)
2368
+ return false;
2369
+ return child.props.some((p) => p.type === NodeTypes.DIRECTIVE &&
2370
+ ['if', 'else-if', 'else', 'show', 'for'].includes(p.name)) ||
2371
+ (inlineTags.has(child.tag) && child.children.some(hasInlineStructure));
2372
+ };
2373
+ const inlineStructure = content.some(hasInlineStructure);
2374
+ if (inlineComponentsFlag && mapping.type === 'group' && !inlineStructure && props.flexDirection === undefined && props.gridColumns === undefined && content.some(readable) && content.every((c) => c.type === NodeTypes.TEXT || c.type === NodeTypes.INTERPOLATION || (c.type === NodeTypes.ELEMENT && inlineTags.has(c.tag))))
2367
2375
  mapping = { type: 'text' };
2368
- if (mapping.type === 'text' && inlineConditional)
2376
+ if (mapping.type === 'text' && inlineStructure)
2369
2377
  mapping = { type: 'group' };
2370
2378
  if (mapping.type === 'text' && content.length === 0 && ['i', 'span', 'b'].includes(node.tag))
2371
2379
  mapping = { type: 'spacer' };
@@ -2680,7 +2688,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
2680
2688
  warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
2681
2689
  const keep = {};
2682
2690
  const src = props;
2683
- for (const k of ['width', 'height', 'widthFraction', 'widthFractionDynamic', 'flex', 'marginTop', 'background', 'gradient', 'radius', 'shadow', 'borderWidth', 'borderColor', 'padding', 'minHeight', 'minWidth', 'opacity']) {
2691
+ // 空盒仍需脱离文档流并保持叠放顺序,避免占位改变父容器布局。
2692
+ for (const k of [
2693
+ 'width', 'height', 'widthFraction', 'widthFractionDynamic', 'flex', 'marginTop',
2694
+ 'background', 'gradient', 'radius', 'shadow', 'borderWidth', 'borderColor',
2695
+ 'padding', 'minHeight', 'minWidth', 'opacity',
2696
+ 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'zIndex',
2697
+ ]) {
2684
2698
  if (src[k] !== undefined)
2685
2699
  keep[k] = src[k];
2686
2700
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dongmingzhixiu/vuexir-compiler",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "Vue SFC → IR → 原生源码(SwiftUI / Compose)编译器核心:纯函数、零 IO",
5
5
  "type": "module",
6
6
  "license": "MIT",