create-bubbles 0.1.23 → 0.1.24
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/index.js +1 -1
- package/package.json +25 -24
- package/template-taro-react-oxc/.env.development +1 -1
- package/template-taro-react-oxc/.env.production +1 -1
- package/template-taro-react-oxc/project.config.json +1 -1
- package/template-taro-vue-eslint/project.config.json +2 -2
- package/{template-vp-monorepo-react-nestjs → template-vp-monorepo-react-hono}/.spaces/server//347/231/273/345/275/225/JWT/344/270/216RefreshToken/350/275/256/346/215/242/351/211/264/346/235/203/346/236/266/346/236/204/345/256/236/347/216/260/346/214/207/345/215/227.md +588 -1133
- package/template-vp-monorepo-react-hono/.vite-hooks/commit-msg +2 -0
- package/template-vp-monorepo-react-hono/.vite-hooks/pre-commit +2 -0
- package/template-vp-monorepo-react-hono/.vscode/extensions.json +3 -0
- package/template-vp-monorepo-react-hono/.vscode/settings.json +23 -0
- package/template-vp-monorepo-react-hono/AGENTS.md +16 -0
- package/template-vp-monorepo-react-hono/README.md +54 -0
- package/template-vp-monorepo-react-hono/apps/server/.env.example +13 -0
- package/template-vp-monorepo-react-hono/apps/server/README.md +75 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle/0000_flaky_madame_masque.sql +18 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle/meta/0000_snapshot.json +153 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle/meta/_journal.json +13 -0
- package/template-vp-monorepo-react-hono/apps/server/drizzle.config.ts +19 -0
- package/template-vp-monorepo-react-hono/apps/server/package.json +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/app.test.ts +57 -0
- package/template-vp-monorepo-react-hono/apps/server/src/app.ts +68 -0
- package/template-vp-monorepo-react-hono/apps/server/src/common/errors/app-error.ts +23 -0
- package/template-vp-monorepo-react-hono/apps/server/src/common/http/response.ts +21 -0
- package/template-vp-monorepo-react-hono/apps/server/src/common/logger.ts +30 -0
- package/template-vp-monorepo-react-hono/apps/server/src/config/env.ts +54 -0
- package/template-vp-monorepo-react-hono/apps/server/src/index.bun.ts +60 -0
- package/template-vp-monorepo-react-hono/apps/server/src/index.ts +73 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/client.ts +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/schema/index.ts +1 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/database/schema/job-runs.ts +42 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/queue/connection.ts +6 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/redis/client.ts +39 -0
- package/template-vp-monorepo-react-hono/apps/server/src/infrastructure/redis/options.ts +28 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/health/health.routes.ts +17 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/health/health.service.ts +59 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.queue.ts +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.repository.ts +101 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.routes.ts +46 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.schemas.ts +9 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.service.ts +57 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.types.ts +11 -0
- package/template-vp-monorepo-react-hono/apps/server/src/modules/jobs/jobs.worker.ts +108 -0
- package/template-vp-monorepo-react-hono/apps/server/src/runtime/shutdown.ts +38 -0
- package/template-vp-monorepo-react-hono/apps/server/src/worker.ts +48 -0
- package/template-vp-monorepo-react-hono/apps/server/tsconfig.json +15 -0
- package/template-vp-monorepo-react-hono/apps/server/vite.config.ts +15 -0
- package/template-vp-monorepo-react-hono/apps/web/.env +2 -0
- package/template-vp-monorepo-react-hono/apps/web/.env.dev +1 -0
- package/template-vp-monorepo-react-hono/apps/web/.env.prod +1 -0
- package/template-vp-monorepo-react-hono/apps/web/index.html +13 -0
- package/template-vp-monorepo-react-hono/apps/web/package.json +35 -0
- package/template-vp-monorepo-react-hono/apps/web/public/favicon.svg +1 -0
- package/template-vp-monorepo-react-hono/apps/web/public/icons.svg +24 -0
- package/template-vp-monorepo-react-hono/apps/web/src/App.tsx +25 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/hero.png +0 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/react.svg +1 -0
- package/template-vp-monorepo-react-hono/apps/web/src/assets/vite.svg +1 -0
- package/template-vp-monorepo-react-hono/apps/web/src/components/Loading/PageLoading.tsx +11 -0
- package/template-vp-monorepo-react-hono/apps/web/src/locales/en_US.json +4 -0
- package/template-vp-monorepo-react-hono/apps/web/src/locales/zh_CN.json +4 -0
- package/template-vp-monorepo-react-hono/apps/web/src/main.tsx +5 -0
- package/template-vp-monorepo-react-hono/apps/web/src/pages/home/index.tsx +47 -0
- package/template-vp-monorepo-react-hono/apps/web/src/router/index.tsx +25 -0
- package/template-vp-monorepo-react-hono/apps/web/src/router/modules/.gitkeep +0 -0
- package/template-vp-monorepo-react-hono/apps/web/src/styles/index.css +7 -0
- package/template-vp-monorepo-react-hono/apps/web/src/types/auto-imports.d.ts +58 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/env/index.ts +6 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/request/core/index.ts +420 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/request/core/utils.ts +39 -0
- package/template-vp-monorepo-react-hono/apps/web/src/utils/request/index.ts +61 -0
- package/template-vp-monorepo-react-hono/apps/web/tsconfig.json +18 -0
- package/template-vp-monorepo-react-hono/apps/web/vite.config.ts +45 -0
- package/template-vp-monorepo-react-hono/commitlint.config.js +1 -0
- package/template-vp-monorepo-react-hono/docker-compose.yml +41 -0
- package/template-vp-monorepo-react-hono/mise.toml +4 -0
- package/template-vp-monorepo-react-hono/package.json +37 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/package.json +20 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/format.ts +9 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/i18n.ts +73 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/index.ts +24 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/storage.ts +47 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/store.ts +48 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/src/types.ts +36 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/tsconfig.json +4 -0
- package/template-vp-monorepo-react-hono/packages/i18n/core/vite.config.ts +13 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/package.json +28 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/src/index.tsx +26 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/src/use-store.ts +14 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/tsconfig.json +9 -0
- package/template-vp-monorepo-react-hono/packages/i18n/react/vite.config.ts +16 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/package.json +27 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/src/index.ts +52 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/src/use-store.ts +19 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/tsconfig.json +8 -0
- package/template-vp-monorepo-react-hono/packages/i18n/vue/vite.config.ts +16 -0
- package/template-vp-monorepo-react-hono/pnpm-workspace.yaml +57 -0
- package/template-vp-monorepo-react-hono/scripts/clean.mjs +48 -0
- package/template-vp-monorepo-react-hono/tsconfig.base.json +10 -0
- package/template-vp-monorepo-react-hono/tsconfig.bundler.json +12 -0
- package/template-vp-monorepo-react-hono/tsconfig.json +11 -0
- package/template-vp-monorepo-react-hono/tsconfig.lib.json +10 -0
- package/template-vp-monorepo-react-hono/vite.config.ts +20 -0
- package/template-vp-monorepo-react-nestjs/.spaces/server/todo.md +991 -0
- package/template-vp-monorepo-react-nestjs/.vscode/launch.json +16 -0
- package/template-vp-monorepo-react-nestjs/.vscode/settings.json +21 -18
- package/template-vp-monorepo-react-nestjs/apps/server/.env.development +3 -39
- package/template-vp-monorepo-react-nestjs/apps/server/.swcrc +8 -0
- package/template-vp-monorepo-react-nestjs/apps/server/drizzle.config.ts +8 -3
- package/template-vp-monorepo-react-nestjs/apps/server/nest-cli.json +8 -1
- package/template-vp-monorepo-react-nestjs/apps/server/package.json +11 -6
- package/template-vp-monorepo-react-nestjs/apps/server/src/app.controller.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/app.module.ts +20 -15
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/adapters/fastify.adapter.ts +16 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/auth.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/response.ts +0 -4
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/session.constants.ts +28 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/current-auth.decorator.ts +16 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/public.decorator.ts +4 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/dto/api-failure.dto.ts +25 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/error/common.error.ts +40 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/exceptions/app.exception.ts +55 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/filters/global-exception.filter.ts +384 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/openapi/response-headers.ts +9 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/index.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/session.config.ts +36 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/database/schema.ts +4 -64
- package/template-vp-monorepo-react-nestjs/apps/server/src/main.ts +29 -8
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.controller.ts +55 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.errors.ts +42 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.module.ts +26 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.repository.ts +40 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/auth.service.ts +129 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/auth-repsponse.dto.ts +31 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/login.dto.ts +11 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/dto/register.dto.ts +17 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/guards/session-auth.guard.ts +50 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/password.service.ts +24 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-store.service.ts +198 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session-token.service.ts +29 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.script.ts +221 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/auth/session/session.types.ts +24 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/db.controller.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/db/dto/create-user.dto.ts +3 -5
- package/template-vp-monorepo-react-nestjs/apps/server/src/modules/test/redis/redis.service.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/server/src/utils/env-arr.ts +3 -0
- package/template-vp-monorepo-react-nestjs/apps/server/test/error-catalog.spec.ts +30 -0
- package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.build.json +11 -1
- package/template-vp-monorepo-react-nestjs/apps/server/tsconfig.json +12 -20
- package/template-vp-monorepo-react-nestjs/apps/server/vite.config.ts +15 -0
- package/template-vp-monorepo-react-nestjs/apps/web/package.json +1 -0
- package/template-vp-monorepo-react-nestjs/apps/web/tsconfig.json +5 -15
- package/template-vp-monorepo-react-nestjs/apps/web-vue/commitlint.config.js +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/lefthook.yml +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/package.json +1 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/api/index.ts +5 -5
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/components/Icon/svg-icon.vue +3 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/lib.ts +6 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/hooks/chart/useEcharts.ts +34 -29
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/i18n/instance.ts +2 -0
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/layout/default/index.vue +17 -32
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/locales/en_US.json +4 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/locales/zh_CN.json +4 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/index.ts +3 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/guard/permissionGuard.ts +7 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/index.ts +7 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/router/interface.ts +6 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/index.ts +6 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/store/modules/user.ts +9 -9
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/styles/index.scss +3 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/types/antdv-next.d.ts +1 -0
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/env.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/index.ts +63 -45
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/core/utils.ts +12 -6
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/utils/request/index.ts +1 -1
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/config.ts +687 -687
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/echart/index.vue +7 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/example/tree-chart.vue +2 -7
- package/template-vp-monorepo-react-nestjs/apps/web-vue/src/views/home/index.vue +13 -3
- package/template-vp-monorepo-react-nestjs/apps/web-vue/tsconfig.json +5 -17
- package/template-vp-monorepo-react-nestjs/apps/web-vue/uno.config.ts +5 -5
- package/template-vp-monorepo-react-nestjs/i18n.config.ts +22 -0
- package/template-vp-monorepo-react-nestjs/package.json +12 -1
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/README.md +113 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/docs/excel-sync-plan.md +618 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/package.json +31 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/cli.ts +5 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/command.ts +512 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/config.ts +15 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/files.ts +67 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/index.ts +19 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/load-config.ts +228 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/report.ts +57 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.test.ts +81 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/scanner.ts +290 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/src/sync.ts +274 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/command.test.ts +209 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/config.test.ts +19 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/load-config.test.ts +194 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/report.test.ts +63 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/sync.test.ts +203 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/test/temporary-directory.ts +25 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/tsconfig.json +9 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/cli/vite.config.ts +18 -0
- package/template-vp-monorepo-react-nestjs/packages/i18n/core/tsconfig.json +1 -12
- package/template-vp-monorepo-react-nestjs/packages/i18n/react/tsconfig.json +3 -10
- package/template-vp-monorepo-react-nestjs/packages/i18n/vue/tsconfig.json +3 -10
- package/template-vp-monorepo-react-nestjs/packages/shared/package.json +40 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/index.ts +13 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/types/auth.ts +35 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/types/common.ts +11 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/types/index.ts +11 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/account.ts +5 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/index.ts +2 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/src/utils/session-terminal.ts +11 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/tsconfig.json +4 -0
- package/template-vp-monorepo-react-nestjs/packages/shared/vite.config.ts +17 -0
- package/template-vp-monorepo-react-nestjs/pnpm-workspace.yaml +6 -6
- package/template-vp-monorepo-react-nestjs/scripts/clean.mjs +48 -0
- package/template-vp-monorepo-react-nestjs/tsconfig.base.json +10 -0
- package/template-vp-monorepo-react-nestjs/tsconfig.bundler.json +12 -0
- package/template-vp-monorepo-react-nestjs/tsconfig.json +8 -6
- package/template-vp-monorepo-react-nestjs/tsconfig.lib.json +10 -0
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/constants/decorator.ts +0 -2
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/decorators/bypass.ts +0 -12
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/interceptors/transform.ts +0 -34
- package/template-vp-monorepo-react-nestjs/apps/server/src/common/model/response.ts +0 -23
- package/template-vp-monorepo-react-nestjs/apps/server/src/config/auth.config.ts +0 -64
- package/template-vp-monorepo-react-nestjs/apps/web-vue/pnpm-workspace.yaml +0 -16
- /package/{template-vp-monorepo-react-nestjs/apps/server/src/config/security.config.ts → template-vp-monorepo-react-hono/.agents/auth.config.ts} +0 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
import { dirname, isAbsolute, resolve } from 'node:path'
|
|
3
|
+
import { pathToFileURL } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import type { I18nConfig } from './config.ts'
|
|
6
|
+
|
|
7
|
+
export const CONFIG_FILE_NAMES = [
|
|
8
|
+
'i18n.config.ts',
|
|
9
|
+
'i18n.config.mts',
|
|
10
|
+
'i18n.config.js',
|
|
11
|
+
'i18n.config.mjs',
|
|
12
|
+
'i18n.config.cts',
|
|
13
|
+
'i18n.config.cjs',
|
|
14
|
+
] as const
|
|
15
|
+
|
|
16
|
+
export interface LoadConfigOptions {
|
|
17
|
+
readonly cwd?: string
|
|
18
|
+
readonly configPath?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LoadedI18nConfig {
|
|
22
|
+
readonly configPath: string
|
|
23
|
+
readonly rootDir: string
|
|
24
|
+
readonly config: I18nConfig
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ConfigNotFoundError extends Error {
|
|
28
|
+
override readonly name = 'ConfigNotFoundError'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ConfigLoadError extends Error {
|
|
32
|
+
override readonly name = 'ConfigLoadError'
|
|
33
|
+
readonly cause: unknown
|
|
34
|
+
|
|
35
|
+
constructor(message: string, cause?: unknown) {
|
|
36
|
+
super(message)
|
|
37
|
+
this.cause = cause
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class ConfigValidationError extends Error {
|
|
42
|
+
override readonly name = 'ConfigValidationError'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function loadConfig(options: LoadConfigOptions = {}): Promise<LoadedI18nConfig> {
|
|
46
|
+
const cwd = resolve(options.cwd ?? process.cwd())
|
|
47
|
+
const configPath =
|
|
48
|
+
options.configPath === undefined
|
|
49
|
+
? await findConfigPath(cwd)
|
|
50
|
+
: await resolveExplicitConfigPath(options.configPath, cwd)
|
|
51
|
+
|
|
52
|
+
let importedConfig: unknown
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const module = (await import(pathToFileURL(configPath).href)) as Record<string, unknown>
|
|
56
|
+
|
|
57
|
+
if (!Object.prototype.hasOwnProperty.call(module, 'default')) {
|
|
58
|
+
throw new ConfigValidationError(
|
|
59
|
+
`Invalid i18n config at "${configPath}": the module must have a default export`,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
importedConfig = module.default
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof ConfigValidationError) {
|
|
66
|
+
throw error
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
throw new ConfigLoadError(`Failed to load i18n config at "${configPath}"`, error)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
configPath,
|
|
74
|
+
rootDir: dirname(configPath),
|
|
75
|
+
config: validateConfig(importedConfig, configPath),
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function validateConfig(value: unknown, configPath = '<config>'): I18nConfig {
|
|
80
|
+
const prefix = `Invalid i18n config at "${configPath}"`
|
|
81
|
+
|
|
82
|
+
if (!isRecord(value)) {
|
|
83
|
+
throw new ConfigValidationError(`${prefix}: the default export must be an object`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const projects = value.projects
|
|
87
|
+
if (!isRecord(projects) || Object.keys(projects).length === 0) {
|
|
88
|
+
throw new ConfigValidationError(`${prefix}: projects must be a non-empty object`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const [projectName, project] of Object.entries(projects)) {
|
|
92
|
+
if (projectName.trim() === '') {
|
|
93
|
+
throw new ConfigValidationError(`${prefix}: project names must be non-empty strings`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const projectPath = `projects[${JSON.stringify(projectName)}]`
|
|
97
|
+
if (!isRecord(project)) {
|
|
98
|
+
throw new ConfigValidationError(`${prefix}: ${projectPath} must be an object`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
validateStringArray(project.include, `${projectPath}.include`, prefix, { nonEmpty: true })
|
|
102
|
+
|
|
103
|
+
if (project.exclude !== undefined) {
|
|
104
|
+
validateStringArray(project.exclude, `${projectPath}.exclude`, prefix)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const catalogs = project.catalogs
|
|
108
|
+
if (!isRecord(catalogs) || Object.keys(catalogs).length === 0) {
|
|
109
|
+
throw new ConfigValidationError(
|
|
110
|
+
`${prefix}: ${projectPath}.catalogs must be a non-empty object`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const [locale, catalogPath] of Object.entries(catalogs)) {
|
|
115
|
+
if (locale.trim() === '') {
|
|
116
|
+
throw new ConfigValidationError(
|
|
117
|
+
`${prefix}: ${projectPath}.catalogs locale names must be non-empty strings`,
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof catalogPath !== 'string' || catalogPath.trim() === '') {
|
|
122
|
+
throw new ConfigValidationError(
|
|
123
|
+
`${prefix}: ${projectPath}.catalogs[${JSON.stringify(locale)}] must be a non-empty string`,
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (value.callNames !== undefined) {
|
|
130
|
+
validateStringArray(value.callNames, 'callNames', prefix, { nonEmpty: true })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
value.report !== undefined &&
|
|
135
|
+
(typeof value.report !== 'string' || value.report.trim() === '')
|
|
136
|
+
) {
|
|
137
|
+
throw new ConfigValidationError(`${prefix}: report must be a non-empty string`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return value as unknown as I18nConfig
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function findConfigPath(cwd: string): Promise<string> {
|
|
144
|
+
let directory = cwd
|
|
145
|
+
|
|
146
|
+
while (true) {
|
|
147
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
148
|
+
const candidate = resolve(directory, fileName)
|
|
149
|
+
if (await isFile(candidate)) {
|
|
150
|
+
return candidate
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const parent = dirname(directory)
|
|
155
|
+
if (parent === directory) {
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
directory = parent
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new ConfigNotFoundError(
|
|
162
|
+
`Could not find an i18n config from "${cwd}" upward. Expected one of: ${CONFIG_FILE_NAMES.join(', ')}`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function resolveExplicitConfigPath(configPath: string, cwd: string): Promise<string> {
|
|
167
|
+
if (configPath.trim() === '') {
|
|
168
|
+
throw new ConfigNotFoundError('The explicit i18n config path must be a non-empty string')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const resolvedPath = isAbsolute(configPath) ? resolve(configPath) : resolve(cwd, configPath)
|
|
172
|
+
if (!(await isFile(resolvedPath))) {
|
|
173
|
+
throw new ConfigNotFoundError(
|
|
174
|
+
`Could not find the explicit i18n config at "${resolvedPath}" (resolved from "${cwd}")`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return resolvedPath
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateStringArray(
|
|
182
|
+
value: unknown,
|
|
183
|
+
path: string,
|
|
184
|
+
prefix: string,
|
|
185
|
+
options: { readonly nonEmpty?: boolean } = {},
|
|
186
|
+
): asserts value is string[] {
|
|
187
|
+
if (!Array.isArray(value)) {
|
|
188
|
+
throw new ConfigValidationError(`${prefix}: ${path} must be an array of non-empty strings`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (options.nonEmpty === true && value.length === 0) {
|
|
192
|
+
throw new ConfigValidationError(`${prefix}: ${path} must be a non-empty array of strings`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const [index, item] of value.entries()) {
|
|
196
|
+
if (typeof item !== 'string' || item.trim() === '') {
|
|
197
|
+
throw new ConfigValidationError(`${prefix}: ${path}[${index}] must be a non-empty string`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
203
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const prototype = Object.getPrototypeOf(value) as unknown
|
|
208
|
+
return prototype === Object.prototype || prototype === null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function isFile(path: string): Promise<boolean> {
|
|
212
|
+
try {
|
|
213
|
+
return (await stat(path)).isFile()
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (isMissingPathError(error)) {
|
|
216
|
+
return false
|
|
217
|
+
}
|
|
218
|
+
throw error
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isMissingPathError(error: unknown): boolean {
|
|
223
|
+
return (
|
|
224
|
+
error instanceof Error &&
|
|
225
|
+
'code' in error &&
|
|
226
|
+
(error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
227
|
+
)
|
|
228
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { mkdir, rename, unlink, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { basename, dirname, join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export interface SyncFileReport {
|
|
6
|
+
project: string
|
|
7
|
+
path: string
|
|
8
|
+
locale: string
|
|
9
|
+
added: string[]
|
|
10
|
+
unused: string[]
|
|
11
|
+
deleted: string[]
|
|
12
|
+
unchangedCount: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SyncReport {
|
|
16
|
+
command: 'sync' | 'check'
|
|
17
|
+
clean: boolean
|
|
18
|
+
dryRun: boolean
|
|
19
|
+
files: SyncFileReport[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CreateSyncReportOptions {
|
|
23
|
+
command?: SyncReport['command']
|
|
24
|
+
clean?: boolean
|
|
25
|
+
dryRun?: boolean
|
|
26
|
+
files?: readonly SyncFileReport[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createSyncReport(options: CreateSyncReportOptions = {}): SyncReport {
|
|
30
|
+
return {
|
|
31
|
+
command: options.command ?? 'sync',
|
|
32
|
+
clean: options.clean ?? false,
|
|
33
|
+
dryRun: options.dryRun ?? false,
|
|
34
|
+
files: (options.files ?? []).map((file) => ({
|
|
35
|
+
...file,
|
|
36
|
+
added: [...file.added],
|
|
37
|
+
unused: [...file.unused],
|
|
38
|
+
deleted: [...file.deleted],
|
|
39
|
+
})),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function writeSyncReport(report: SyncReport, path: string): Promise<void> {
|
|
44
|
+
const content = `${JSON.stringify(report, undefined, 2)}\n`
|
|
45
|
+
const directory = dirname(path)
|
|
46
|
+
const temporaryPath = join(directory, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`)
|
|
47
|
+
|
|
48
|
+
await mkdir(directory, { recursive: true })
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await writeFile(temporaryPath, content, 'utf8')
|
|
52
|
+
await rename(temporaryPath, path)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
await unlink(temporaryPath).catch(() => undefined)
|
|
55
|
+
throw error
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vite-plus/test'
|
|
2
|
+
|
|
3
|
+
import { scanSource } from './scanner.ts'
|
|
4
|
+
|
|
5
|
+
describe('scanSource', () => {
|
|
6
|
+
it('extracts Chinese text and all three static quote styles', () => {
|
|
7
|
+
const source = ["tr('保存')", 'tr("Open")', 'tr(`用户`)'].join('\n')
|
|
8
|
+
|
|
9
|
+
expect(scanSource(source)).toEqual([
|
|
10
|
+
{ key: '保存', index: 0, line: 1, column: 1 },
|
|
11
|
+
{ key: 'Open', index: 9, line: 2, column: 1 },
|
|
12
|
+
{ key: '用户', index: 20, line: 3, column: 1 },
|
|
13
|
+
])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('allows whitespace and newlines around the first argument', () => {
|
|
17
|
+
const source = ['const message = tr', ' (', " '你好 {name}',", ' { name },', ' )'].join(
|
|
18
|
+
'\n',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
expect(scanSource(source)).toEqual([{ key: '你好 {name}', index: 16, line: 1, column: 17 }])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('decodes common escapes without evaluating source code', () => {
|
|
25
|
+
const source = String.raw`tr('It\'s\n中\t\x41\u6587\u{1F600}')`
|
|
26
|
+
|
|
27
|
+
expect(scanSource(source).map(({ key }) => key)).toEqual(["It's\n中\tA文😀"])
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('allows escaped interpolation syntax in a static template', () => {
|
|
31
|
+
const source = 'tr(`price: \\${value}`)'
|
|
32
|
+
|
|
33
|
+
expect(scanSource(source).map(({ key }) => key)).toEqual(['price: ${value}'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('ignores dynamic arguments, concatenation, and template interpolation', () => {
|
|
37
|
+
const source = [
|
|
38
|
+
'tr(key)',
|
|
39
|
+
'tr(getKey())',
|
|
40
|
+
"tr('a' + value)",
|
|
41
|
+
'tr(`hello ${name}`)',
|
|
42
|
+
"tr('kept')",
|
|
43
|
+
].join('\n')
|
|
44
|
+
|
|
45
|
+
expect(scanSource(source).map(({ key }) => key)).toEqual(['kept'])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('requires an identifier boundary around the call name', () => {
|
|
49
|
+
const source = [
|
|
50
|
+
"notr('prefix')",
|
|
51
|
+
"trailingtr('suffix')",
|
|
52
|
+
"tr2('number')",
|
|
53
|
+
"_tr('underscore')",
|
|
54
|
+
"obj.tr('method')",
|
|
55
|
+
"tr('direct')",
|
|
56
|
+
].join('\n')
|
|
57
|
+
|
|
58
|
+
expect(scanSource(source).map(({ key }) => key)).toEqual(['method', 'direct'])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('supports custom call names', () => {
|
|
62
|
+
const source = ["t('short')", "$t('dollar')", "tr('default')"].join('\n')
|
|
63
|
+
|
|
64
|
+
expect(scanSource(source, { callNames: ['t', '$t'] }).map(({ key }) => key)).toEqual([
|
|
65
|
+
'short',
|
|
66
|
+
'dollar',
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('returns every occurrence and leaves deduplication to the caller', () => {
|
|
71
|
+
const source = "tr('same')\ntr('same')"
|
|
72
|
+
|
|
73
|
+
expect(scanSource(source).map(({ key }) => key)).toEqual(['same', 'same'])
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('rejects a static string followed by an unexpected token', () => {
|
|
77
|
+
const source = ["tr('plus' + value)", "tr('semicolon'; value)"].join('\n')
|
|
78
|
+
|
|
79
|
+
expect(scanSource(source)).toEqual([])
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
export interface MessageOccurrence {
|
|
2
|
+
key: string
|
|
3
|
+
index: number
|
|
4
|
+
line: number
|
|
5
|
+
column: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ScanOptions {
|
|
9
|
+
callNames?: readonly string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ParsedString {
|
|
13
|
+
value: string
|
|
14
|
+
endIndex: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ParsedEscape {
|
|
18
|
+
value: string
|
|
19
|
+
nextIndex: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const identifierContinuePattern = /[\p{ID_Continue}_$]/u
|
|
23
|
+
const whitespacePattern = /\s/u
|
|
24
|
+
const hexPattern = /^[\dA-Fa-f]+$/
|
|
25
|
+
|
|
26
|
+
export function scanSource(source: string, options: ScanOptions = {}): MessageOccurrence[] {
|
|
27
|
+
const callNames = normalizeCallNames(options.callNames)
|
|
28
|
+
const occurrences: MessageOccurrence[] = []
|
|
29
|
+
|
|
30
|
+
if (callNames.length === 0) {
|
|
31
|
+
return occurrences
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lineStarts = collectLineStarts(source)
|
|
35
|
+
|
|
36
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
37
|
+
for (const callName of callNames) {
|
|
38
|
+
if (!source.startsWith(callName, index)) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!hasIdentifierBoundaries(source, index, callName)) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let cursor = skipWhitespace(source, index + callName.length)
|
|
47
|
+
if (source[cursor] !== '(') {
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cursor = skipWhitespace(source, cursor + 1)
|
|
52
|
+
const quote = source[cursor]
|
|
53
|
+
if (quote !== "'" && quote !== '"' && quote !== '`') {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parsed = parseString(source, cursor, quote)
|
|
58
|
+
if (!parsed) {
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const nextTokenIndex = skipWhitespace(source, parsed.endIndex)
|
|
63
|
+
const nextToken = source[nextTokenIndex]
|
|
64
|
+
if (nextToken !== ',' && nextToken !== ')') {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const position = locatePosition(lineStarts, index)
|
|
69
|
+
occurrences.push({
|
|
70
|
+
key: parsed.value,
|
|
71
|
+
index,
|
|
72
|
+
line: position.line,
|
|
73
|
+
column: position.column,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
index += callName.length - 1
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return occurrences
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeCallNames(callNames: readonly string[] | undefined): string[] {
|
|
85
|
+
const names = callNames ?? ['tr']
|
|
86
|
+
return [...new Set(names.filter((name) => name.length > 0))].sort(
|
|
87
|
+
(left, right) => right.length - left.length,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasIdentifierBoundaries(source: string, index: number, callName: string): boolean {
|
|
92
|
+
const firstCharacter = callName[0]
|
|
93
|
+
const lastCharacter = callName[callName.length - 1]
|
|
94
|
+
|
|
95
|
+
if (isIdentifierContinue(firstCharacter) && isIdentifierContinue(source[index - 1])) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return !(
|
|
100
|
+
isIdentifierContinue(lastCharacter) && isIdentifierContinue(source[index + callName.length])
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isIdentifierContinue(character: string | undefined): boolean {
|
|
105
|
+
return (
|
|
106
|
+
character !== undefined &&
|
|
107
|
+
(identifierContinuePattern.test(character) ||
|
|
108
|
+
character === '\u{200C}' ||
|
|
109
|
+
character === '\u{200D}')
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function skipWhitespace(source: string, startIndex: number): number {
|
|
114
|
+
let index = startIndex
|
|
115
|
+
while (index < source.length && whitespacePattern.test(source[index] ?? '')) {
|
|
116
|
+
index += 1
|
|
117
|
+
}
|
|
118
|
+
return index
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseString(
|
|
122
|
+
source: string,
|
|
123
|
+
startIndex: number,
|
|
124
|
+
quote: "'" | '"' | '`',
|
|
125
|
+
): ParsedString | undefined {
|
|
126
|
+
let value = ''
|
|
127
|
+
|
|
128
|
+
for (let index = startIndex + 1; index < source.length; ) {
|
|
129
|
+
const character = source[index]
|
|
130
|
+
|
|
131
|
+
if (character === quote) {
|
|
132
|
+
return { value, endIndex: index + 1 }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (character === '\\') {
|
|
136
|
+
const escape = parseEscape(source, index)
|
|
137
|
+
if (!escape) {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
value += escape.value
|
|
141
|
+
index = escape.nextIndex
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (quote === '`' && character === '$' && source[index + 1] === '{') {
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (quote !== '`' && (character === '\n' || character === '\r')) {
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
value += character
|
|
154
|
+
index += 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseEscape(source: string, slashIndex: number): ParsedEscape | undefined {
|
|
161
|
+
const escaped = source[slashIndex + 1]
|
|
162
|
+
if (escaped === undefined) {
|
|
163
|
+
return undefined
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const simpleEscapes: Readonly<Record<string, string>> = {
|
|
167
|
+
"'": "'",
|
|
168
|
+
'"': '"',
|
|
169
|
+
'`': '`',
|
|
170
|
+
'\\': '\\',
|
|
171
|
+
$: '$',
|
|
172
|
+
b: '\b',
|
|
173
|
+
f: '\f',
|
|
174
|
+
n: '\n',
|
|
175
|
+
r: '\r',
|
|
176
|
+
t: '\t',
|
|
177
|
+
v: '\v',
|
|
178
|
+
0: '\0',
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (escaped in simpleEscapes) {
|
|
182
|
+
return {
|
|
183
|
+
value: simpleEscapes[escaped] ?? '',
|
|
184
|
+
nextIndex: slashIndex + 2,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (escaped === '\n') {
|
|
189
|
+
return { value: '', nextIndex: slashIndex + 2 }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (escaped === '\r') {
|
|
193
|
+
const nextIndex = source[slashIndex + 2] === '\n' ? slashIndex + 3 : slashIndex + 2
|
|
194
|
+
return { value: '', nextIndex }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (escaped === 'x') {
|
|
198
|
+
return parseFixedHexEscape(source, slashIndex, 2, 2)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (escaped === 'u') {
|
|
202
|
+
if (source[slashIndex + 2] === '{') {
|
|
203
|
+
return parseCodePointEscape(source, slashIndex)
|
|
204
|
+
}
|
|
205
|
+
return parseFixedHexEscape(source, slashIndex, 2, 4)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { value: escaped, nextIndex: slashIndex + 2 }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function parseFixedHexEscape(
|
|
212
|
+
source: string,
|
|
213
|
+
slashIndex: number,
|
|
214
|
+
prefixLength: number,
|
|
215
|
+
digitCount: number,
|
|
216
|
+
): ParsedEscape | undefined {
|
|
217
|
+
const digitsStart = slashIndex + prefixLength
|
|
218
|
+
const digits = source.slice(digitsStart, digitsStart + digitCount)
|
|
219
|
+
if (digits.length !== digitCount || !hexPattern.test(digits)) {
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
value: String.fromCodePoint(Number.parseInt(digits, 16)),
|
|
225
|
+
nextIndex: digitsStart + digitCount,
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function parseCodePointEscape(source: string, slashIndex: number): ParsedEscape | undefined {
|
|
230
|
+
const digitsStart = slashIndex + 3
|
|
231
|
+
const closingBraceIndex = source.indexOf('}', digitsStart)
|
|
232
|
+
if (closingBraceIndex === -1) {
|
|
233
|
+
return undefined
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const digits = source.slice(digitsStart, closingBraceIndex)
|
|
237
|
+
if (digits.length === 0 || digits.length > 6 || !hexPattern.test(digits)) {
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const codePoint = Number.parseInt(digits, 16)
|
|
242
|
+
if (codePoint > 0x10_ff_ff) {
|
|
243
|
+
return undefined
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
value: String.fromCodePoint(codePoint),
|
|
248
|
+
nextIndex: closingBraceIndex + 1,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function collectLineStarts(source: string): number[] {
|
|
253
|
+
const starts = [0]
|
|
254
|
+
|
|
255
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
256
|
+
if (source[index] === '\r') {
|
|
257
|
+
if (source[index + 1] === '\n') {
|
|
258
|
+
index += 1
|
|
259
|
+
}
|
|
260
|
+
starts.push(index + 1)
|
|
261
|
+
} else if (source[index] === '\n') {
|
|
262
|
+
starts.push(index + 1)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return starts
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function locatePosition(
|
|
270
|
+
lineStarts: readonly number[],
|
|
271
|
+
index: number,
|
|
272
|
+
): { line: number; column: number } {
|
|
273
|
+
let low = 0
|
|
274
|
+
let high = lineStarts.length - 1
|
|
275
|
+
|
|
276
|
+
while (low <= high) {
|
|
277
|
+
const middle = Math.floor((low + high) / 2)
|
|
278
|
+
const lineStart = lineStarts[middle] ?? 0
|
|
279
|
+
|
|
280
|
+
if (lineStart <= index) {
|
|
281
|
+
low = middle + 1
|
|
282
|
+
} else {
|
|
283
|
+
high = middle - 1
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const lineIndex = Math.max(0, high)
|
|
288
|
+
const lineStart = lineStarts[lineIndex] ?? 0
|
|
289
|
+
return { line: lineIndex + 1, column: index - lineStart + 1 }
|
|
290
|
+
}
|