uniweb 1.0.2 → 1.2.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/package.json +4 -4
- package/partials/agents.md +2 -1
- package/src/backend/foundation-bring-along.js +17 -7
- package/src/commands/add.js +119 -69
- package/src/commands/build.js +64 -138
- package/src/commands/clone.js +15 -3
- package/src/commands/dev.js +57 -0
- package/src/commands/doctor.js +48 -44
- package/src/commands/rename.js +11 -26
- package/src/commands/update.js +19 -5
- package/src/framework-index.json +16 -16
- package/src/utils/config.js +98 -90
- package/src/utils/discover.js +17 -20
- package/src/utils/placement.js +55 -0
- package/src/utils/scaffold.js +1 -1
- package/src/utils/workspace-packages.js +431 -0
- package/src/utils/workspace.js +10 -71
- package/src/versions.js +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "https://github.com/uniweb/cli/issues"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=22.12"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"handlebars": "^4.7.8",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/content-writer": "~1.0.1",
|
|
45
|
-
"@uniweb/core": "~1.0
|
|
45
|
+
"@uniweb/core": "~1.1.0",
|
|
46
46
|
"@uniweb/semantic-parser": "~1.5.1",
|
|
47
47
|
"@uniweb/schemas": "~1.0.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.
|
|
50
|
+
"@uniweb/build": "~1.1.1",
|
|
51
51
|
"@uniweb/semantic-parser": "~1.5.1",
|
|
52
52
|
"@uniweb/content-reader": "~1.2.8"
|
|
53
53
|
},
|
package/partials/agents.md
CHANGED
|
@@ -194,6 +194,7 @@ The rest of this guide explains how Uniweb works. This part is what to do *first
|
|
|
194
194
|
| `src`, `site` | **single** (the default) | foundation in `src/`, site in `site/` |
|
|
195
195
|
| `foundations/*`, `sites/*` | **segregated** | several sites may share one foundation — a foundation edit hits all of them |
|
|
196
196
|
| `*/src`, `*/site` | **co-located projects** | one self-contained pair per project: `docs/src` + `docs/site` |
|
|
197
|
+
| `*/src`, `*/sites/*` | **projects with several sites** | each project's foundation with its sites: `memberships/src` + `memberships/sites/fitness`, `memberships/sites/society` — a foundation edit hits every site in its project |
|
|
197
198
|
| a package whose `main.js` says `extension: true` — `effects`, `extensions/*` (alongside any of the above) | extensions present | extra section types, runtime-loaded — see step 2 |
|
|
198
199
|
|
|
199
200
|
Those names are convention; the globs are the truth. Full guide with the wiring for each: `development/project-structures.md`.
|
|
@@ -2801,7 +2802,7 @@ npx uniweb@latest update # apply: align @uniweb/* deps AND refresh A
|
|
|
2801
2802
|
|
|
2802
2803
|
**`npx uniweb@latest update` is the command for bringing a project up to date.** It aligns the project's `@uniweb/*` dependencies *and* this AGENTS.md to the version matrix of the CLI that runs it. Deps and documentation move together — that's the whole point of the verb.
|
|
2803
2804
|
|
|
2804
|
-
**What the version numbers mean.** A patch brings fixes and features; a minor version is where Uniweb puts breaking changes — `update` lists each package that crosses one, so
|
|
2805
|
+
**What the version numbers mean.** A patch brings fixes and features; a minor version is where Uniweb puts breaking changes — `update` lists each package that crosses one, so build and check the project before shipping — and a major is kept for something really big. Dependencies are written as tilde ranges (`~1.0.1`), so `pnpm install` and `pnpm update` never cross a minor on their own. The move from 0.x to 1.0.1 asks nothing of a project that was on the latest 0.x versions — 1.0.1 carried every package's 0.x line over — though `update` marks each package `major · BREAKING`.
|
|
2805
2806
|
|
|
2806
2807
|
> ### ⛔ WRITE `@latest`. A BARE `uniweb update` IS USUALLY A NO-OP, AND IT SAYS SO CONVINCINGLY
|
|
2807
2808
|
>
|
|
@@ -59,7 +59,8 @@ import {
|
|
|
59
59
|
detectFoundationType,
|
|
60
60
|
isExtensionUrl,
|
|
61
61
|
readFoundationName,
|
|
62
|
-
checkFoundationName
|
|
62
|
+
checkFoundationName,
|
|
63
|
+
findSiteExtension
|
|
63
64
|
} from '@uniweb/build'
|
|
64
65
|
import { computeFoundationDigest } from '../utils/code-upload.js'
|
|
65
66
|
import { isNonInteractive } from '../utils/interactive.js'
|
|
@@ -127,15 +128,24 @@ export function resolveLocalExtensions(siteDir, siteYml) {
|
|
|
127
128
|
? entry
|
|
128
129
|
: null
|
|
129
130
|
if (!decl || isExtensionUrl(decl)) continue
|
|
130
|
-
|
|
131
|
+
// Resolved as the primary foundation is — and, where that finds nothing, where
|
|
132
|
+
// extensions live: the rule the build follows for a site-relative extension
|
|
133
|
+
// (`findSiteExtension`), which looks in `extensions/<name>/` and in the folders
|
|
134
|
+
// above the site. It takes a plain name only, so a catalog ref stays not local.
|
|
135
|
+
// ⛔ Until 2026-10-13 only the first: a site naming the `extensions` template's
|
|
136
|
+
// own `extensions/effects` — as publish's refusal of `/effects/entry.js`
|
|
137
|
+
// advises — released nothing and sent an unpinned ref the host cannot serve.
|
|
138
|
+
let dir = null
|
|
131
139
|
try {
|
|
132
|
-
info = detectFoundationType(decl, siteDir)
|
|
140
|
+
const info = detectFoundationType(decl, siteDir)
|
|
141
|
+
if (info?.type === 'local' && info.path) dir = info.path
|
|
133
142
|
} catch {
|
|
134
|
-
//
|
|
135
|
-
continue
|
|
143
|
+
// Not a foundation's shape — tried as an extension below.
|
|
136
144
|
}
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
dir ??= findSiteExtension(decl, siteDir)
|
|
146
|
+
// Still nothing: the site build surfaces the canonical error; nothing local.
|
|
147
|
+
if (!dir) continue
|
|
148
|
+
out.push({ decl, dir, version: readPkgField(dir, 'version') })
|
|
139
149
|
}
|
|
140
150
|
return out
|
|
141
151
|
}
|
package/src/commands/add.js
CHANGED
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
* uniweb add section <name> [--foundation <name>]
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { existsSync } from 'node:fs'
|
|
14
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
15
15
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
16
16
|
import { join, relative, basename } from 'node:path'
|
|
17
17
|
import prompts from 'prompts'
|
|
18
18
|
import yaml from 'js-yaml'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
resolveFoundationSrcPath,
|
|
21
|
+
readFoundationName,
|
|
22
|
+
findSiteExtension
|
|
23
|
+
} from '@uniweb/build'
|
|
20
24
|
import {
|
|
21
25
|
scaffoldFoundation,
|
|
22
26
|
scaffoldSite,
|
|
@@ -26,15 +30,12 @@ import {
|
|
|
26
30
|
} from '../utils/scaffold.js'
|
|
27
31
|
import {
|
|
28
32
|
resolvePlacement,
|
|
33
|
+
projectConflictLines,
|
|
29
34
|
SITE_KIND,
|
|
30
35
|
FOUNDATION_KIND,
|
|
31
36
|
EXTENSION_KIND
|
|
32
37
|
} from '../utils/placement.js'
|
|
33
|
-
import {
|
|
34
|
-
readWorkspaceConfig,
|
|
35
|
-
addWorkspaceGlob,
|
|
36
|
-
updateRootScripts
|
|
37
|
-
} from '../utils/config.js'
|
|
38
|
+
import { registerWorkspacePackage, updateRootScripts } from '../utils/config.js'
|
|
38
39
|
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
39
40
|
import { generateStarter, reportStarter, declarationFor } from './starter.js'
|
|
40
41
|
import {
|
|
@@ -360,6 +361,7 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
360
361
|
// both. Format validation runs on the derived package name below, not
|
|
361
362
|
// on the raw input — slashes in the input are intentional path syntax.
|
|
362
363
|
const placement = resolvePlacement(rootDir, name, opts, FOUNDATION_KIND)
|
|
364
|
+
refuseProjectConflict(placement, 'foundation', name, opts)
|
|
363
365
|
if (placement.outsideRoot) {
|
|
364
366
|
error(`--path must name a folder inside the workspace: ${placement.outsideRoot}`)
|
|
365
367
|
log('')
|
|
@@ -470,10 +472,10 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
470
472
|
ensureFoundationName(fullPath, registryName)
|
|
471
473
|
}
|
|
472
474
|
|
|
473
|
-
// Register the package
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
await
|
|
475
|
+
// Register the package by its exact path — unless a pattern the workspace lists
|
|
476
|
+
// already covers it (`foundations/*`, `*/src`), which leaves the file as it is.
|
|
477
|
+
// No glob inference: a pattern is the user's to write.
|
|
478
|
+
await registerWorkspacePackage(rootDir, relativePath)
|
|
477
479
|
|
|
478
480
|
// Update root scripts
|
|
479
481
|
const sites = await discoverSites(rootDir)
|
|
@@ -486,6 +488,18 @@ async function addFoundation(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
486
488
|
log(`Next: ${colors.cyan}${installCmd(pm)}${colors.reset}`)
|
|
487
489
|
}
|
|
488
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Stop when `--project` came with a name or a `--path` — it names the folder itself,
|
|
493
|
+
* so the two disagree. Refused, never ignored (`projectConflictLines`).
|
|
494
|
+
*/
|
|
495
|
+
function refuseProjectConflict(placement, kind, name, opts) {
|
|
496
|
+
if (!placement.projectConflict) return
|
|
497
|
+
const [first, ...rest] = projectConflictLines(kind, name, opts, `${getCliPrefix()} add ${kind}`)
|
|
498
|
+
error(first)
|
|
499
|
+
for (const line of rest) log(line)
|
|
500
|
+
process.exit(1)
|
|
501
|
+
}
|
|
502
|
+
|
|
489
503
|
/**
|
|
490
504
|
* Add a site to the workspace
|
|
491
505
|
*/
|
|
@@ -495,6 +509,7 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
495
509
|
|
|
496
510
|
// Resolve placement first (path + package name); see notes in addFoundation.
|
|
497
511
|
const placement = resolvePlacement(rootDir, name, opts, SITE_KIND)
|
|
512
|
+
refuseProjectConflict(placement, 'site', name, opts)
|
|
498
513
|
if (placement.outsideRoot) {
|
|
499
514
|
error(`--path must name a folder inside the workspace: ${placement.outsideRoot}`)
|
|
500
515
|
log('')
|
|
@@ -608,8 +623,8 @@ async function addSite(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
608
623
|
await applyFromTemplate(opts.from, 'site', fullPath, projectName)
|
|
609
624
|
}
|
|
610
625
|
|
|
611
|
-
// Register the package by exact path
|
|
612
|
-
await
|
|
626
|
+
// Register the package by exact path, unless a listed pattern covers it already.
|
|
627
|
+
await registerWorkspacePackage(rootDir, relativePath)
|
|
613
628
|
|
|
614
629
|
// Update root scripts (discover sites after registration — includes the new one)
|
|
615
630
|
const sites = await discoverSites(rootDir)
|
|
@@ -678,6 +693,7 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
678
693
|
// you name is the folder created — `effects` → effects/, `extensions/effects` →
|
|
679
694
|
// extensions/effects/ — and `--path` is the folder it goes in.
|
|
680
695
|
const placement = resolvePlacement(rootDir, name, opts, EXTENSION_KIND)
|
|
696
|
+
refuseProjectConflict(placement, 'extension', name, opts)
|
|
681
697
|
if (placement.outsideRoot) {
|
|
682
698
|
error(`--path must name a folder inside the workspace: ${placement.outsideRoot}`)
|
|
683
699
|
log('')
|
|
@@ -739,7 +755,7 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
739
755
|
}
|
|
740
756
|
|
|
741
757
|
// Register the folder it is in, as add foundation does
|
|
742
|
-
await
|
|
758
|
+
await registerWorkspacePackage(rootDir, relativePath)
|
|
743
759
|
|
|
744
760
|
// Wire extension to site:
|
|
745
761
|
// - --site <name>: explicit, wire it.
|
|
@@ -750,60 +766,57 @@ async function addExtension(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
750
766
|
// warning so the user/agent knows wiring is pending, and exit 0
|
|
751
767
|
// (the extension itself is fine).
|
|
752
768
|
// - no sites: print a note and exit 0.
|
|
769
|
+
// ⭐ And only when that site can load it (extensionUnreachable): an entry the
|
|
770
|
+
// build cannot serve would build with a warning and 404 at runtime.
|
|
753
771
|
let wiredSite = null
|
|
754
772
|
let unwiredReason = null
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
value: s.name
|
|
787
|
-
}))
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
onCancel: () => {
|
|
791
|
-
log('\nCancelled.')
|
|
792
|
-
process.exit(0)
|
|
793
|
-
}
|
|
773
|
+
let target = null
|
|
774
|
+
const sites = await discoverSites(rootDir)
|
|
775
|
+
if (opts.site) {
|
|
776
|
+
target = sites.find((s) => s.name === opts.site) || null
|
|
777
|
+
if (!target) {
|
|
778
|
+
unwiredReason = `Could not find site '${opts.site}' to wire this extension to. Available: ${sites.map((s) => s.name).join(', ') || 'none'}.`
|
|
779
|
+
}
|
|
780
|
+
} else if (sites.length === 1) {
|
|
781
|
+
target = sites[0]
|
|
782
|
+
} else if (sites.length > 1) {
|
|
783
|
+
if (isNonInteractive(process.argv)) {
|
|
784
|
+
unwiredReason = `Multiple sites in workspace; extension not wired. Re-run with --site <name>, or edit <site>/site.yml::extensions: manually.`
|
|
785
|
+
} else {
|
|
786
|
+
const sortedSites = [...sites].sort((a, b) =>
|
|
787
|
+
a.name.localeCompare(b.name)
|
|
788
|
+
)
|
|
789
|
+
const response = await prompts(
|
|
790
|
+
{
|
|
791
|
+
type: 'select',
|
|
792
|
+
name: 'site',
|
|
793
|
+
message: 'Which site is this extension for?',
|
|
794
|
+
choices: sortedSites.map((s) => ({
|
|
795
|
+
title: s.name,
|
|
796
|
+
description: s.path,
|
|
797
|
+
value: s.name
|
|
798
|
+
}))
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
onCancel: () => {
|
|
802
|
+
log('\nCancelled.')
|
|
803
|
+
process.exit(0)
|
|
794
804
|
}
|
|
795
|
-
)
|
|
796
|
-
if (response.site) {
|
|
797
|
-
wiredSite = await wireExtensionToSite(rootDir, response.site, relativePath)
|
|
798
805
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
806
|
+
)
|
|
807
|
+
target = sites.find((s) => s.name === response.site) || null
|
|
808
|
+
}
|
|
809
|
+
} else {
|
|
810
|
+
unwiredReason = `No site in this workspace yet. Wire this extension into a site's site.yml::extensions: once you create one.`
|
|
811
|
+
}
|
|
812
|
+
if (target) {
|
|
813
|
+
unwiredReason = extensionUnreachable(rootDir, target, name, fullPath)
|
|
814
|
+
if (!unwiredReason) {
|
|
815
|
+
wiredSite = await wireExtensionToSite(rootDir, target.name, relativePath)
|
|
802
816
|
}
|
|
803
817
|
}
|
|
804
818
|
|
|
805
|
-
// Update root scripts
|
|
806
|
-
const sites = await discoverSites(rootDir)
|
|
819
|
+
// Update root scripts (an extension adds no site, so the list read above stands)
|
|
807
820
|
await updateRootScripts(rootDir, sites, pm)
|
|
808
821
|
|
|
809
822
|
let msg = `Created extension '${name}' at ${relativePath}/`
|
|
@@ -932,9 +945,10 @@ async function addProject(rootDir, projectName, opts, pm = 'pnpm') {
|
|
|
932
945
|
)
|
|
933
946
|
}
|
|
934
947
|
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
await
|
|
948
|
+
// The co-located patterns — each added only if what it lists is not covered yet
|
|
949
|
+
// (a workspace listing `*/src` and `*/sites/*` keeps its file as it is for `src/`).
|
|
950
|
+
await registerWorkspacePackage(rootDir, `${name}/src`, { entry: '*/src' })
|
|
951
|
+
await registerWorkspacePackage(rootDir, `${name}/site`, { entry: '*/site' })
|
|
938
952
|
|
|
939
953
|
// Update root scripts
|
|
940
954
|
const sites = await discoverSites(rootDir)
|
|
@@ -1124,6 +1138,41 @@ async function applyFromTemplate(
|
|
|
1124
1138
|
}
|
|
1125
1139
|
}
|
|
1126
1140
|
|
|
1141
|
+
/**
|
|
1142
|
+
* Why `site` cannot load the extension just made at `extensionDir` — or null when
|
|
1143
|
+
* it can. A site loads `/<name>/entry.js` from the folder `@uniweb/build` finds for
|
|
1144
|
+
* `<name>` (`findSiteExtension`): `<name>/` or `extensions/<name>/`, in the folder
|
|
1145
|
+
* the site is in or one above it, up to the workspace root. The build, prerender
|
|
1146
|
+
* and the dev server follow that rule, so `add` asks it rather than restating it.
|
|
1147
|
+
*
|
|
1148
|
+
* ⛔ Until 2026-10-13 this was a copy of the rule, judged from the workspace root
|
|
1149
|
+
* while the build looked only in the folder the site is in — so a site at
|
|
1150
|
+
* `sites/main` was wired to `extensions/effects`, and its build could not find it.
|
|
1151
|
+
*
|
|
1152
|
+
* @param {string} rootDir
|
|
1153
|
+
* @param {{ name: string, path: string }} site - `path` relative to `rootDir`
|
|
1154
|
+
* @param {string} name - the extension's folder name: its URL's first segment
|
|
1155
|
+
* @param {string} extensionDir - absolute
|
|
1156
|
+
* @returns {string|null}
|
|
1157
|
+
*/
|
|
1158
|
+
function extensionUnreachable(rootDir, site, name, extensionDir) {
|
|
1159
|
+
const found = findSiteExtension(name, join(rootDir, site.path))
|
|
1160
|
+
if (found && realpathSync(found) === realpathSync(extensionDir)) return null
|
|
1161
|
+
const here = relative(rootDir, extensionDir)
|
|
1162
|
+
if (found) {
|
|
1163
|
+
return (
|
|
1164
|
+
`Site '${site.name}' would load the nearer extension named '${name}', at ` +
|
|
1165
|
+
`${relative(rootDir, found)}/ — not ${here}/. Rename one of them.`
|
|
1166
|
+
)
|
|
1167
|
+
}
|
|
1168
|
+
return (
|
|
1169
|
+
`A site loads a workspace extension from /${name}/entry.js, which the build finds in ` +
|
|
1170
|
+
`${name}/ or extensions/${name}/ in the folder the site is in or a folder above it, up to ` +
|
|
1171
|
+
`the workspace root. For ${site.path}/ that is not ${here}/: move the folder there, or list a ` +
|
|
1172
|
+
`URL where the built extension is served under ${site.path}/site.yml::extensions:.`
|
|
1173
|
+
)
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1127
1176
|
/**
|
|
1128
1177
|
* Wire an extension URL to a site's site.yml
|
|
1129
1178
|
*/
|
|
@@ -1147,10 +1196,11 @@ async function wireExtensionToSite(rootDir, siteName, extensionFolder) {
|
|
|
1147
1196
|
const config = yaml.load(content) || {}
|
|
1148
1197
|
|
|
1149
1198
|
// ⭐ The site loads it from its own origin at `/<name>/entry.js`, `<name>`
|
|
1150
|
-
// being the extension's folder: the build finds that extension
|
|
1151
|
-
//
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1199
|
+
// being the extension's folder: the build finds that extension by name
|
|
1200
|
+
// (`@uniweb/build`, findSiteExtension — the caller has checked it finds THIS
|
|
1201
|
+
// one), copies its build into the site's output there, and prerender and the
|
|
1202
|
+
// dev server load it from the same place — the form the `extensions`
|
|
1203
|
+
// template declares.
|
|
1154
1204
|
// ⛔ Until 2026-10-08 this wrote `/<folder>/dist/entry.js`, which that lookup
|
|
1155
1205
|
// reads as an extension named after the folder's FIRST segment —
|
|
1156
1206
|
// `/extensions/effects/dist/entry.js` as one named `extensions` — so the
|
|
@@ -1589,7 +1639,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
|
|
|
1589
1639
|
}
|
|
1590
1640
|
|
|
1591
1641
|
// Read node version from root package.json engines.node if pinned;
|
|
1592
|
-
// otherwise default to
|
|
1642
|
+
// otherwise default to 22, the framework's own floor (>=22.12).
|
|
1593
1643
|
const rootPkg = JSON.parse(
|
|
1594
1644
|
await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
|
|
1595
1645
|
)
|