refactor(mp-harmony): add HAR dependency checks and debug logs

1. add debug logs for diy component lifecycle and data flow
2. add required HAR dependency declarations for mp-harmony entry module
3. add har-deps config, validation script and documentation
This commit is contained in:
2026-07-30 11:43:25 +08:00
parent 3bbf8c9531
commit df9d45fc1d
8 changed files with 579 additions and 20 deletions

View File

@@ -232,6 +232,7 @@ export default {
}
}
console.log('[DIY][init] calling getDiyInfo, storeToken=', !!this.storeToken);
await this.getDiyInfo();
//获取启动广告
await this.getDiyAdv();
@@ -362,10 +363,15 @@ export default {
// 节流:正在请求 / N 秒内已加载过 / 语言未变 -> 跳过
const now = Date.now();
const currentLang = uni.getStorageSync("lang") || '';
console.log('[DIY][getDiyInfo] start', 'force=', force, 'id=', this.id, 'name=', this.name, 'lang=', currentLang, 'loading=', this.diyInfoLoading, 'lastAt=', this.lastDiyInfoTime, 'lastLang=', this.lastDiyInfoLang);
if (!force) {
if (this.diyInfoLoading) return;
if (this.diyInfoLoading) {
console.log('[DIY][getDiyInfo] skip: already loading');
return;
}
if (this.lastDiyInfoTime && (now - this.lastDiyInfoTime) < this.diyInfoTtl
&& this.lastDiyInfoLang === currentLang) {
console.log('[DIY][getDiyInfo] skip: throttled by ttl');
return;
}
}
@@ -386,7 +392,7 @@ export default {
async: false
});
} catch (e) {
console.error('getDiyInfo request failed', e);
console.error('[DIY][getDiyInfo] request failed', e);
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
this.diyInfoLoading = false;
// 无法从网络获取数据时,页面内显示错误横幅并提供重试(带详细原因 + 请求地址)
@@ -395,8 +401,10 @@ export default {
return;
}
this.diyInfoLoading = false;
console.log('[DIY][getDiyInfo] got response', 'code=', res && res.code, 'hasData=', !!(res && res.data), 'valueType=', res && res.data && typeof res.data.value, 'valueLen=', res && res.data && (typeof res.data.value === 'string' ? res.data.value.length : (res.data.value ? res.data.value.length : null)));
if (!res || res.code != 0 || !res.data) {
console.warn('[DIY][getDiyInfo] invalid response', JSON.stringify(res));
if (this.$refs.loadingCover) this.$refs.loadingCover.hide();
// 完全没有拿到网络响应,视为无法从网络获取数据,页面内显示错误横幅
@@ -435,7 +443,13 @@ export default {
let diyDataValue = res.data;
if (diyDataValue.value) {
this.diyData = JSON.parse(diyDataValue.value);
try {
this.diyData = JSON.parse(diyDataValue.value);
} catch (parseErr) {
console.error('[DIY][getDiyInfo] JSON.parse(value) failed', parseErr, 'value preview=', String(diyDataValue.value).slice(0, 200));
throw parseErr;
}
console.log('[DIY][getDiyInfo] parsed diyData', 'globalKeys=', this.diyData && this.diyData.global ? Object.keys(this.diyData.global) : null, 'valueCount=', this.diyData && this.diyData.value && this.diyData.value.length, 'topIndexValue before=', this.topIndexValue);
// 导航栏标题要根据语言环境切换
// 优先使用 title_map 多语言 map,没有则回退到 title / en_title
@@ -523,6 +537,9 @@ export default {
this.openBottomNav = this.diyData.global.openBottomNav;
}
this.isShowCopyRight = true;
console.log('[DIY][getDiyInfo] done', 'topIndexValue after=', this.topIndexValue && this.topIndexValue.componentName, 'valueCount=', this.diyData.value.length, 'openBottomNav=', this.openBottomNav, 'isShowCopyRight=', this.isShowCopyRight);
} else {
console.warn('[DIY][getDiyInfo] no value in res.data', JSON.stringify(diyDataValue).slice(0, 200));
}
},
closePopupWindow() {

View File

@@ -306,9 +306,11 @@ export default {
}
},
created() {
console.log('[DIY-GROUP][created] hasDiyData=', !!this.diyData, 'valueLen=', this.diyData && this.diyData.value && this.diyData.value.length);
this.processData();
},
mounted() {
console.log('[DIY-GROUP][mounted] hasDiyData=', !!this.diyData, 'valueLen=', this.diyData && this.diyData.value && this.diyData.value.length);
if (this.diyData && this.diyData.value) {
this._notifyLoaded();
}
@@ -349,11 +351,13 @@ export default {
},
methods: {
_notifyLoaded() {
if (this._loadedTimer) clearTimeout(this._loadedTimer);
this._loadedTimer = setTimeout(() => {
this.$emit('loaded');
}, 300);
},
console.log('[DIY-GROUP][_notifyLoaded] enter, will emit loaded in 300ms');
if (this._loadedTimer) clearTimeout(this._loadedTimer);
this._loadedTimer = setTimeout(() => {
console.log('[DIY-GROUP][_notifyLoaded] emit loaded');
this.$emit('loaded');
}, 300);
},
processData() {
if (!this.diyGlobalData || !this.diyGlobalData.value) {
this.processedData = [];

View File

@@ -0,0 +1,73 @@
# 鸿蒙元服务 entry 模块依赖说明
> ⚠️ **本模块的 HAR 依赖必须保留**,否则运行时会缺失子包 chunk(DIY 首页、商品分类、订单、营销页全部失效)。
## 背景
`mp-harmony` 项目把 uniapp 编译产物拆成 6 个鸿蒙模块:
| 模块名 | srcPath | 类型 | 内含 |
|---|---|---|---|
| entry | `./entry` | 主 HAP | 入口页面、首页骨架、TabBar |
| components_diy | `./ascf/ascf_build/components_diy` | shared HAR | 50+ diy-* 组件的 pack.js |
| pages_goods | `./ascf/ascf_build/pages_goods` | shared HAR | 商品分类/详情/列表/购物车 |
| pages_order | `./ascf/ascf_build/pages_order` | shared HAR | 订单/支付/详情 |
| pages_promotion | `./ascf/ascf_build/pages_promotion` | shared HAR | 分销/积分/商户 |
| pages_tool | `./ascf/ascf_build/pages_tool` | shared HAR | 会员/支付/客服/工具页 |
`entry` 必须把这 5 个 HAR 加为依赖,否则运行时鸿蒙端加载 entry HAP 时找不到
`components-diy/diy-group.pack.js` 等子包 chunk,所有 diy 组件被
`componentPlaceholder: view` 替换成空 view,DIY 内容不显示,12s splash 兜底超时。
## 必须保留的依赖
`entry/oh-package.json5` 的 `dependencies` 必须包含:
```json5
{
"components_diy": "file:../ascf/ascf_build/components_diy",
"pages_goods": "file:../ascf/ascf_build/pages_goods",
"pages_order": "file:../ascf/ascf_build/pages_order",
"pages_promotion": "file:../ascf/ascf_build/pages_promotion",
"pages_tool": "file:../ascf/ascf_build/pages_tool",
}
```
## 防回归
```bash
# 检查 module.json + oh_modules + oh-package.json5 是否完整
node scripts/check-har-deps.js --rawfile
# 严格模式: 任何缺失即 exit 1 (CI 用)
node scripts/check-har-deps.js --rawfile --strict
```
校验脚本读取 `./har-deps-required.json5`(白名单单一事实来源)。
## 如果你需要新增/删除子包
1. 修改 `ascf_build/<新包>/oh-package.json5` 的 `name` 字段
2. 在 `mp-harmony/build-profile.json5` 的 `modules` 数组里增/减模块
3. 在 `entry/oh-package.json5` 的 `dependencies` 里增/减 file: 依赖
4. **同步更新 `entry/har-deps-required.json5` 的 `requiredHarDeps` / `ohModulesRoots`**
5. 在 DevEco Studio 中 Sync 项目 → 重新 Build → 运行 `node scripts/check-har-deps.js --rawfile --strict`
6. 通知 ascf 负责人对齐
## 常见误操作
- ❌ 在 DevEco 的 "Manage Dependencies" 对话框里只保留 `@atomicservice/ascfapi`
→ 5 个 file: 依赖会被清空,运行失败
- ❌ 删除 `entry/oh-package.json5` 的 file: 依赖后用 ohpm install 重装
→ ohpm 会重新同步 file: 依赖,**只要 file: 声明还在就是安全的**
- ❌ 把 `ascf_build/<pkg>` 目录改名/移走
→ file: 引用会变成 broken,运行失败
## 故障现象速查
| 现象 | 原因 | 修复 |
|---|---|---|
| 首页只显示 TabBar,无 DIY 内容 | components_diy 缺失 | 加 file: 依赖 + Sync + Build |
| 12s splash 后页面空白 | 任意一个 HAR 子包缺失 | 跑校验脚本看哪个缺失 |
| 商品分类页空白 | pages_goods 缺失 | 同上 |
| "应用未关联服务空间" 报错 | uniCloud 服务未关联(与本依赖无关) | uniCloud 目录右键关联服务空间 |

View File

@@ -0,0 +1,63 @@
{
// ============================================================================
// 鸿蒙元服务 entry 模块的必需 HAR 依赖白名单
//
// 背景:
// mp-harmony 项目把 uniapp 编译产物拆成了 6 个鸿蒙模块:
// - entry (主入口 HAP, 由 DevEco 编译)
// - components_diy (50+ diy-* 组件的 pack.js, shared HAR)
// - pages_goods (商品分类/详情/列表/购物车, shared HAR)
// - pages_order (订单相关页面, shared HAR)
// - pages_promotion (分销/积分/商户等营销页面, shared HAR)
// - pages_tool (会员/支付/客服/工具页, shared HAR)
//
// entry 的 module.json 默认只会声明 oh-package.json5 里 dependencies 中的 HAR
// 作为 module dependencies。如果这里不显式声明, 鸿蒙运行时会找不到
// components-diy/diy-group.pack.js 等子包 chunk,
// DIY 首页 / 商品分类 / 订单 / 营销页 全部被 componentPlaceholder fallback 成 <view>,
// 表现为: 首页 DIY 内容不显示, 12s splash 兜底超时, TabBar 点击无反应。
//
// 必须:
// entry/oh-package.json5 的 dependencies 必须保留以下 5 个 file: 依赖。
// 不要轻易删除, 删除前请先和 ascf 子包负责人对齐。
//
// 验证:
// 执行 `node scripts/check-har-deps.js` 校验当前 module.json 是否包含这些依赖。
// ============================================================================
requiredHarDeps: [
"components_diy",
"pages_goods",
"pages_order",
"pages_promotion",
"pages_tool",
],
// oh-package.json5 里依赖声明的预期 source 字符串
// (DevEco/ohpm 解析后会规范化, 这里保留原始声明便于核对)
expectedDependencySources: {
components_diy: "file:../ascf/ascf_build/components_diy",
pages_goods: "file:../ascf/ascf_build/pages_goods",
pages_order: "file:../ascf/ascf_build/pages_order",
pages_promotion: "file:../ascf/ascf_build/pages_promotion",
pages_tool: "file:../ascf/ascf_build/pages_tool",
},
// 校验脚本要扫描的产物路径 (相对 entry 目录)
buildArtifacts: {
moduleJson: "build/default/intermediates/package/default/module.json",
},
// oh_modules 子包内 rawfile 根目录映射 (用于 --rawfile 检查)
// 子包 HAR 通过 ohpm 同步后落在 entry/oh_modules/<pkg>/src/main/resources/rawfile/<root>/
// HAR 资源在运行时由鸿蒙端通过 HAR 共享机制加载, 不是 entry 的 rawfile 路径
ohModulesRoots: {
components_diy: {
pkg: "components_diy",
root: "components-diy",
},
pages_goods: { pkg: "pages_goods", root: "pages_goods" },
pages_order: { pkg: "pages_order", root: "pages_order" },
pages_promotion:{ pkg: "pages_promotion",root: "pages_promotion" },
pages_tool: { pkg: "pages_tool", root: "pages_tool" },
},
}

View File

@@ -6,7 +6,12 @@
"lockfileVersion": 3,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@atomicservice/ascfapi@^1.0.6": "@atomicservice/ascfapi@1.0.23"
"@atomicservice/ascfapi@^1.0.6": "@atomicservice/ascfapi@1.0.23",
"components_diy@../ascf/ascf_build/components_diy": "components_diy@../ascf/ascf_build/components_diy",
"pages_goods@../ascf/ascf_build/pages_goods": "pages_goods@../ascf/ascf_build/pages_goods",
"pages_order@../ascf/ascf_build/pages_order": "pages_order@../ascf/ascf_build/pages_order",
"pages_promotion@../ascf/ascf_build/pages_promotion": "pages_promotion@../ascf/ascf_build/pages_promotion",
"pages_tool@../ascf/ascf_build/pages_tool": "pages_tool@../ascf/ascf_build/pages_tool"
},
"packages": {
"@atomicservice/ascfapi@1.0.23": {
@@ -16,6 +21,41 @@
"resolved": "https://ohpm.openharmony.cn/ohpm/@atomicservice/ascfapi/-/ascfapi-1.0.23.har",
"registryType": "ohpm",
"packageType": "InterfaceHar"
},
"components_diy@../ascf/ascf_build/components_diy": {
"name": "components_diy",
"version": "1.0.0",
"resolved": "../ascf/ascf_build/components_diy",
"registryType": "local",
"packageType": "InterfaceHar"
},
"pages_goods@../ascf/ascf_build/pages_goods": {
"name": "pages_goods",
"version": "1.0.0",
"resolved": "../ascf/ascf_build/pages_goods",
"registryType": "local",
"packageType": "InterfaceHar"
},
"pages_order@../ascf/ascf_build/pages_order": {
"name": "pages_order",
"version": "1.0.0",
"resolved": "../ascf/ascf_build/pages_order",
"registryType": "local",
"packageType": "InterfaceHar"
},
"pages_promotion@../ascf/ascf_build/pages_promotion": {
"name": "pages_promotion",
"version": "1.0.0",
"resolved": "../ascf/ascf_build/pages_promotion",
"registryType": "local",
"packageType": "InterfaceHar"
},
"pages_tool@../ascf/ascf_build/pages_tool": {
"name": "pages_tool",
"version": "1.0.0",
"resolved": "../ascf/ascf_build/pages_tool",
"registryType": "local",
"packageType": "InterfaceHar"
}
}
}

View File

@@ -1,12 +1,28 @@
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"modelVersion": "6.0.0",
"main": "",
"author": "",
"license": "",
"dependencies": {
"@atomicservice/ascfapi": "^1.0.6"
}
// ============================================================================
// 鸿蒙元服务 entry 模块依赖声明
//
// ⚠️ 不要随意删除下面 5 个 file: 依赖。
// 它们是 mp-harmony 项目的 5 个 shared HAR 子包 (components_diy / pages_goods /
// pages_order / pages_promotion / pages_tool), 内含 uniapp 编译产物的 *.pack.js。
//
// 详见 ./har-deps-required.json5 与 ./scripts/check-har-deps.js。
//
// 如要新增/删除子包, 请同步更新 har-deps-required.json5 并通知 ascf 负责人。
// ============================================================================
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"modelVersion": "6.0.0",
"main": "",
"author": "",
"license": "",
"dependencies": {
"@atomicservice/ascfapi": "^1.0.6",
"components_diy": "file:../ascf/ascf_build/components_diy",
"pages_goods": "file:../ascf/ascf_build/pages_goods",
"pages_order": "file:../ascf/ascf_build/pages_order",
"pages_promotion": "file:../ascf/ascf_build/pages_promotion",
"pages_tool": "file:../ascf/ascf_build/pages_tool"
}
}

View File

@@ -0,0 +1,202 @@
#!/usr/bin/env node
/* eslint-disable no-console */
/**
* 鸿蒙元服务 entry 模块依赖校验脚本
*
* 目的:
* 防止 entry 的 HAP 打包时丢失 5 个 ascf shared HAR 子包依赖,
* 否则运行时会找不到 components-diy/diy-group.pack.js 等子包 chunk,
* DIY 首页 + 商品分类 + 订单 + 营销页全部失效 (componentPlaceholder fallback 成 view)。
*
* 使用:
* node scripts/check-har-deps.js # 仅校验 entry/build/.../module.json
* node scripts/check-har-deps.js --rawfile # 同时校验 rawfile 资源是否就位
* node scripts/check-har-deps.js --strict # 任一缺失即 exit 1
*
* 默认情况:
* 缺失时仅打印警告 (CI 友好, 不阻塞开发), 加 --strict 才严格失败。
*
* 依赖:
* ./har-deps-required.json5 (本脚本的单一事实来源)
*/
'use strict';
const fs = require('fs');
const path = require('path');
const parseJson5 = require('./load-json5');
const ENTRY_DIR = path.resolve(__dirname, '..');
const CONFIG_PATH = path.join(ENTRY_DIR, 'har-deps-required.json5');
function loadJson5(filePath) {
const raw = fs.readFileSync(filePath, 'utf8');
return parseJson5(raw);
}
function exists(p) {
try {
fs.accessSync(p, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
function listDir(p) {
try {
return fs.readdirSync(p);
} catch {
return [];
}
}
function color(s, c) {
const codes = {
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
gray: '\x1b[90m',
reset: '\x1b[0m',
};
return process.stdout.isTTY ? codes[c] + s + codes.reset : s;
}
function main() {
const args = process.argv.slice(2);
const checkRawfile = args.includes('--rawfile');
const strict = args.includes('--strict');
if (!exists(CONFIG_PATH)) {
console.error(color(`✘ 缺少白名单配置: ${CONFIG_PATH}`, 'red'));
process.exit(strict ? 1 : 0);
}
const config = loadJson5(CONFIG_PATH);
const requiredHarDeps = config.requiredHarDeps || [];
const expectedSources = config.expectedDependencySources || {};
const moduleJsonRel = (config.buildArtifacts && config.buildArtifacts.moduleJson)
|| 'build/default/intermediates/package/default/module.json';
const moduleJsonPath = path.join(ENTRY_DIR, moduleJsonRel);
console.log(color('=== HAR 子包依赖校验 ===', 'cyan'));
console.log(`配置白名单: ${requiredHarDeps.join(', ')}`);
console.log(`module.json: ${moduleJsonPath}`);
console.log();
const errors = [];
const warnings = [];
// ---- 1. 校验 module.json dependencies ----
if (!exists(moduleJsonPath)) {
warnings.push(`module.json 不存在 (${moduleJsonPath}), 跳过依赖检查。可能尚未 Build。`);
} else {
let mj;
try {
mj = JSON.parse(fs.readFileSync(moduleJsonPath, 'utf8'));
} catch (e) {
errors.push(`module.json 解析失败: ${e.message}`);
}
if (mj) {
const deps = (mj.module && mj.module.dependencies) || mj.dependencies || [];
const depNames = new Set(deps.map(d => d.moduleName));
for (const har of requiredHarDeps) {
if (depNames.has(har)) {
console.log(color(` ✔ module.json.dependencies 包含 ${har}`, 'green'));
} else {
errors.push(`module.json.dependencies 缺失 ${har} (实际: ${[...depNames].join(', ') || '空'})`);
console.log(color(` ✘ module.json.dependencies 缺失 ${har}`, 'red'));
}
}
}
}
// ---- 2. 校验 oh_modules 子包资源是否就位 (可选) ----
if (checkRawfile) {
console.log();
console.log(color('--- oh_modules 子包资源检查 ---', 'cyan'));
const ohModulesRoots = (config.ohModulesRoots) || {};
const ohModulesDir = path.join(ENTRY_DIR, 'oh_modules');
if (!exists(ohModulesDir)) {
warnings.push(`oh_modules 目录不存在 (${ohModulesDir}), 请先在 DevEco 中 Sync 项目`);
}
for (const har of requiredHarDeps) {
const cfg = ohModulesRoots[har];
if (!cfg) continue;
const subDir = path.join(ohModulesDir, cfg.pkg, 'src', 'main', 'resources', 'rawfile', cfg.root);
if (!exists(subDir)) {
errors.push(`oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 缺失 (鸿蒙运行时会找不到组件)`);
console.log(color(` ✘ oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 缺失`, 'red'));
} else {
const files = listDir(subDir);
const packFiles = files.filter(f => f.endsWith('.pack.js'));
const inner = listDir(subDir).filter(f => {
try { return fs.statSync(path.join(subDir, f)).isDirectory(); } catch { return false; }
});
const totalPack = packFiles.length + inner.reduce((acc, d) => {
try { return acc + fs.readdirSync(path.join(subDir, d)).filter(f => f.endsWith('.pack.js')).length; }
catch { return acc; }
}, 0);
if (totalPack === 0) {
warnings.push(`oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 存在但无 .pack.js`);
console.log(color(` ⚠ oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 无 .pack.js`, 'yellow'));
} else {
console.log(color(` ✔ oh_modules/${cfg.pkg}/.../rawfile/${cfg.root} 存在, ${totalPack} 个 .pack.js`, 'green'));
}
}
}
}
// ---- 3. 校验 oh-package.json5 的 dependencies 与白名单一致 ----
const ohPkgPath = path.join(ENTRY_DIR, 'oh-package.json5');
if (exists(ohPkgPath)) {
console.log();
console.log(color('--- oh-package.json5 检查 ---', 'cyan'));
const ohPkg = loadJson5(ohPkgPath);
const ohDeps = ohPkg.dependencies || {};
for (const har of requiredHarDeps) {
if (!(har in ohDeps)) {
errors.push(`oh-package.json5.dependencies 缺失 ${har}`);
console.log(color(` ✘ oh-package.json5 缺失 ${har}`, 'red'));
} else {
const expected = expectedSources[har];
const actual = ohDeps[har];
if (expected && actual !== expected) {
warnings.push(`oh-package.json5 中 ${har} 的 source 不是预期: ${actual} (预期 ${expected})`);
console.log(color(` ⚠ ${har} source 异常: ${actual}`, 'yellow'));
} else {
console.log(color(` ✔ oh-package.json5 包含 ${har} (${actual})`, 'green'));
}
}
}
}
// ---- 总结 ----
console.log();
if (warnings.length) {
console.log(color(`警告 (${warnings.length}):`, 'yellow'));
for (const w of warnings) console.log(' - ' + w);
}
if (errors.length) {
console.log();
console.log(color(`错误 (${errors.length}):`, 'red'));
for (const e of errors) console.log(' - ' + e);
console.log();
console.log(color('✘ 校验失败: entry 模块缺少必要的 HAR 子包依赖。', 'red'));
console.log(color(' 原因: 这会导致运行时找不到 components-diy/diy-group.pack.js 等子包 chunk,', 'gray'));
console.log(color(' DIY 首页 + 商品分类 + 订单 + 营销页全部失效, 12s splash 兜底超时。', 'gray'));
console.log(color(' 修复: 在 entry/oh-package.json5 的 dependencies 里补齐 5 个 file: 依赖,', 'gray'));
console.log(color(' 同步 ./har-deps-required.json5, 然后重新 Build。', 'gray'));
process.exit(strict ? 1 : 0);
}
console.log(color('✔ 校验通过', 'green'));
}
try {
main();
} catch (e) {
console.error(color(`脚本异常: ${e.stack || e.message}`, 'red'));
process.exit(1);
}

View File

@@ -0,0 +1,144 @@
/**
* 简单 json5 解析 (支持 // 单行注释, /* 多行注释 *\/, 字符串, 转义)
* 不支持: 数字下划线, 十六进制等扩展
*/
'use strict';
function parseJson5(text) {
let i = 0;
const len = text.length;
function peek() { return text[i]; }
function next() { return text[i++]; }
function skipWsAndComments() {
while (i < len) {
const c = text[i];
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
i++;
continue;
}
if (c === '/' && text[i + 1] === '/') {
while (i < len && text[i] !== '\n') i++;
continue;
}
if (c === '/' && text[i + 1] === '*') {
i += 2;
while (i < len && !(text[i] === '*' && text[i + 1] === '/')) i++;
i += 2;
continue;
}
break;
}
}
function readString() {
const quote = next(); // " or '
let out = '';
while (i < len) {
const c = next();
if (c === '\\') {
const esc = next();
switch (esc) {
case 'n': out += '\n'; break;
case 't': out += '\t'; break;
case 'r': out += '\r'; break;
case '\\': out += '\\'; break;
case '"': out += '"'; break;
case "'": out += "'"; break;
case '/': out += '/'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'u': {
const hex = text.substr(i, 4);
i += 4;
out += String.fromCharCode(parseInt(hex, 16));
break;
}
default: out += esc;
}
continue;
}
if (c === quote) return out;
out += c;
}
throw new Error('unterminated string');
}
function readValue() {
skipWsAndComments();
const c = peek();
if (c === '"' || c === "'") return readString();
if (c === '{') return readObject();
if (c === '[') return readArray();
if (c === 't' || c === 'f') return readBoolOrNull();
if (c === 'n') return readBoolOrNull();
if (c === '-' || (c >= '0' && c <= '9')) return readNumber();
throw new Error(`unexpected char '${c}' at ${i}`);
}
function readObject() {
next(); // {
const obj = {};
while (true) {
skipWsAndComments();
if (peek() === '}') { next(); return obj; }
// key
let key;
if (peek() === '"' || peek() === "'") key = readString();
else {
// bareword (json5 allows unquoted keys)
let s = '';
while (i < len && /[A-Za-z0-9_$-]/.test(text[i])) s += text[i++];
key = s;
}
skipWsAndComments();
if (next() !== ':') throw new Error(`expected ':' at ${i}`);
obj[key] = readValue();
skipWsAndComments();
const nc = next();
if (nc === ',') continue;
if (nc === '}') return obj;
throw new Error(`expected , or } at ${i}`);
}
}
function readArray() {
next(); // [
const arr = [];
while (true) {
skipWsAndComments();
if (peek() === ']') { next(); return arr; }
arr.push(readValue());
skipWsAndComments();
const nc = next();
if (nc === ',') continue;
if (nc === ']') return arr;
throw new Error(`expected , or ] at ${i}`);
}
}
function readBoolOrNull() {
if (text.substr(i, 4) === 'true') { i += 4; return true; }
if (text.substr(i, 5) === 'false') { i += 5; return false; }
if (text.substr(i, 4) === 'null') { i += 4; return null; }
throw new Error(`bad literal at ${i}`);
}
function readNumber() {
let s = '';
if (peek() === '-') s += next();
while (i < len && /[0-9.eE+-]/.test(text[i])) s += next();
const n = parseFloat(s);
if (isNaN(n)) throw new Error(`bad number '${s}'`);
return n;
}
skipWsAndComments();
const result = readValue();
skipWsAndComments();
if (i < len) throw new Error(`trailing chars at ${i}: ${text.substr(i, 30)}`);
return result;
}
module.exports = parseJson5;