@nuxtjp/localized-site 0.1.0 → 0.1.2
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/README.ja.md +66 -0
- package/README.md +22 -26
- package/bin/nuxtjp-localized-site.mjs +9 -1
- package/dist/module.json +1 -1
- package/organization/app.vue +5 -0
- package/organization/cli.mjs +58 -0
- package/organization/index.d.mts +12 -0
- package/organization/index.mjs +77 -0
- package/organization/workspace.mjs +31 -0
- package/package.json +35 -13
- package/security/README.md +17 -0
- package/security/apply.mjs +19 -0
- package/security/dependency-security.check.cjs +51 -0
- package/security/patches/braces@3.0.3.patch +92 -0
- package/security/patches/node-forge@1.4.0.patch +20 -0
- package/security/project.mjs +61 -0
- package/security/resolved-dependency.cjs +40 -0
package/README.ja.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @nuxtjp/localized-site
|
|
2
|
+
|
|
3
|
+
[English](README.md)
|
|
4
|
+
|
|
5
|
+
日本語・英語の紹介サイトを、1つの検証済み設定から構成します。`/`、`/docs`、`/docs/getting-started`、`/docs/security`と各英語ページを生成し、言語切替とSEOを適用します。
|
|
6
|
+
|
|
7
|
+
## 導入と設定
|
|
8
|
+
|
|
9
|
+
`0.1.0`はnpmで公開済みです。Node.js 22.19以降または24.11以降、Nuxt 4.5.1以降の4系、Vue `^3.5.40`を使用してください。
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add --save-exact @nuxtjp/localized-site@0.1.0 nuxt@4.5.1 vue@3.5.43
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
export default defineNuxtConfig({
|
|
17
|
+
modules: [['@nuxtjp/localized-site', { contentRoot: '.' }]]
|
|
18
|
+
})
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`contentRoot`を明示します。[汎用設定サンプル](examples/content/site.config.json)を、そのディレクトリに`site.config.json`として置き、サイト固有の内容に置き換えてください。
|
|
22
|
+
`siteId`、`serviceId`、`origin`、`audienceScope`、完全なja/enコンテンツ、ちょうど3つのfeatures、導入・セキュリティ文書、privacy/contact説明、`externalActions: false`が必須です。
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm exec nuxtjp-localized-site validate --content-root .
|
|
26
|
+
pnpm exec nuxt build
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 動作と制約
|
|
30
|
+
|
|
31
|
+
privacy/contactは説明文として表示し、未設定のリンクや操作を生成しません。
|
|
32
|
+
メンテナンス表示は、ブラウザから同一originの`/maintenance.json`を読みます。取得・形式・期限の検証に失敗した場合は表示せず、サイト本文と言語切替は利用できます。
|
|
33
|
+
配布物だけを試す場合は、registry版の代わりに承認された正確なTGZを導入してください。この手順はサイトの公開やregistry設定を行いません。
|
|
34
|
+
|
|
35
|
+
## 型付きメッセージ
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const messages = defineLocalizedMessages({
|
|
39
|
+
ja: { save: '保存', cancel: 'キャンセル' },
|
|
40
|
+
en: { save: 'Save', cancel: 'Cancel' }
|
|
41
|
+
})
|
|
42
|
+
const { t } = useLocalizedMessages(messages)
|
|
43
|
+
t('save')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
英語のキーは日本語と一致させます。空のメッセージは`fallbackLocale`、次に`defaultLocale`へfallbackします。
|
|
47
|
+
|
|
48
|
+
## 開発
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pnpm install --frozen-lockfile
|
|
52
|
+
pnpm test
|
|
53
|
+
pnpm typecheck
|
|
54
|
+
pnpm build
|
|
55
|
+
pnpm validate:example
|
|
56
|
+
pnpm pack
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`prepack`はテスト、型検査、build、サンプル検証を実行します。リリース前に、実際のarchiveを別hostで確認してください。
|
|
60
|
+
|
|
61
|
+
## 文書とライセンス
|
|
62
|
+
|
|
63
|
+
[使い方](https://github.com/nuxtjp/nuxt-localized-site/blob/main/docs/getting-started.md) · [Schema](schemas/localized-site-v1.schema.json) · [テスト](https://github.com/nuxtjp/nuxt-localized-site/tree/main/test) · [セキュリティ報告](https://github.com/nuxtjp/nuxt-localized-site/blob/main/SECURITY.md)
|
|
64
|
+
|
|
65
|
+
コードは[Apache-2.0](LICENSE)です。[LICENSE-PREVIOUS](LICENSE-PREVIOUS)に以前の許諾、[NOTICE](NOTICE)に帰属表示を保持します。
|
|
66
|
+
[LICENSE-ASSETS](LICENSE-ASSETS)は対象となるブランド・コンテンツ素材の別条件を保持します。コードのライセンスを変更せず、第三者やサイト所有者のコンテンツの権利を追加で許諾するものでもありません。
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @nuxtjp/localized-site
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[日本語](README.ja.md)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
`/`、`/docs`、`/docs/getting-started`、`/docs/security`と各英語ページを生成し、言語切替とSEOを適用します。
|
|
5
|
+
Generate a Japanese/English introduction site from one validated configuration. The module supplies `/`, `/docs`, `/docs/getting-started`, `/docs/security` and their English counterparts, locale links and SEO metadata.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
## Install and configure
|
|
8
|
+
|
|
9
|
+
Version `0.1.0` is published on npm. Use Node.js 22.19+ or 24.11+, Nuxt 4.5.1+ in major 4, and Vue `^3.5.40`.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
12
|
pnpm add --save-exact @nuxtjp/localized-site@0.1.0 nuxt@4.5.1 vue@3.5.43
|
|
@@ -18,29 +18,18 @@ export default defineNuxtConfig({
|
|
|
18
18
|
})
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
`siteId
|
|
21
|
+
Set `contentRoot` explicitly. Copy the [generic configuration](examples/content/site.config.json) there as `site.config.json`, and replace it with the site's own content.
|
|
22
|
+
The configuration requires `siteId`, `serviceId`, `origin`, `audienceScope`, complete ja/en content, exactly three features, getting-started and security documents, privacy/contact notices, and `externalActions: false`.
|
|
23
23
|
|
|
24
24
|
```sh
|
|
25
25
|
pnpm exec nuxtjp-localized-site validate --content-root .
|
|
26
26
|
pnpm exec nuxt build
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
メンテナンス表示はブラウザから同一originの`/maintenance.json`を読みます。取得・形式・期限の検証に失敗した場合は表示せず、本文と言語切替は利用できます。
|
|
31
|
-
配布物だけを試す際は、registry版の代わりに承認されたTGZをインストールしてください。サイトの公開やregistry設定はこの手順に含みません。
|
|
32
|
-
|
|
33
|
-
## English
|
|
34
|
-
|
|
35
|
-
Generate a Japanese/English introduction site from one validated configuration.
|
|
36
|
-
The module supplies landing, overview, getting-started and security pages, locale links and SEO metadata.
|
|
37
|
-
|
|
38
|
-
Publication is pending. After verifying registry availability, use the installation command above with Node.js 22.19+ or 24.11+, Nuxt 4.5.1+ in major 4, and Vue `^3.5.40`.
|
|
39
|
-
Set `contentRoot` explicitly, copy the generic example to that directory as `site.config.json`, replace its content, validate it, then build the host application.
|
|
40
|
-
The configuration requires site/service identifiers, origin, audience scope, complete ja/en content, exactly three features, documents and privacy/contact notices, and `externalActions: false`.
|
|
29
|
+
## Behavior and limits
|
|
41
30
|
|
|
42
|
-
Privacy/contact notices are text
|
|
43
|
-
In the browser, the optional maintenance notice reads same-origin `/maintenance.json`. Failed retrieval, invalid data or expired data hides
|
|
31
|
+
Privacy/contact notices are text. The module creates no unset links or actions.
|
|
32
|
+
In the browser, the optional maintenance notice reads same-origin `/maintenance.json`. Failed retrieval, invalid data or expired data hides that notice; the site and locale switching remain usable.
|
|
44
33
|
For archive testing, install the exact reviewed TGZ instead of the registry version. These commands do not publish a website or configure registry access.
|
|
45
34
|
|
|
46
35
|
## Typed messages
|
|
@@ -56,7 +45,7 @@ t('save')
|
|
|
56
45
|
|
|
57
46
|
English keys must match Japanese keys. Empty messages fall back to `fallbackLocale`, then `defaultLocale`.
|
|
58
47
|
|
|
59
|
-
## Development
|
|
48
|
+
## Development
|
|
60
49
|
|
|
61
50
|
```sh
|
|
62
51
|
pnpm install --frozen-lockfile
|
|
@@ -67,8 +56,15 @@ pnpm validate:example
|
|
|
67
56
|
pnpm pack
|
|
68
57
|
```
|
|
69
58
|
|
|
70
|
-
`prepack` runs
|
|
71
|
-
|
|
59
|
+
`prepack` runs tests, typecheck, build and example validation. Verify the actual archive in a separate host before a release.
|
|
60
|
+
|
|
61
|
+
## Documentation and license
|
|
62
|
+
|
|
63
|
+
[Usage guide](https://github.com/nuxtjp/nuxt-localized-site/blob/main/docs/getting-started.md) · [Schema](schemas/localized-site-v1.schema.json) · [Tests](https://github.com/nuxtjp/nuxt-localized-site/tree/main/test) · [Security reporting](https://github.com/nuxtjp/nuxt-localized-site/blob/main/SECURITY.md)
|
|
64
|
+
|
|
65
|
+
Code: [Apache-2.0](LICENSE). [LICENSE-PREVIOUS](LICENSE-PREVIOUS) retains prior grants, and [NOTICE](NOTICE) preserves attribution.
|
|
66
|
+
[LICENSE-ASSETS](LICENSE-ASSETS) retains separate terms for covered brand/content assets. It does not change the code license or grant rights to third-party or site-owned content.
|
|
67
|
+
|
|
68
|
+
## Consumer dependency security
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
[LICENSE-ASSETS](LICENSE-ASSETS) retains separate terms for covered brand/content assets; it does not change the code license or grant rights to third-party/site-owned content.
|
|
70
|
+
See [dependency security backports](security/README.md) before installing this package in a Nuxt application. pnpm consumers must explicitly apply the included backports and verify their locked dependency tree; ordinary npm installation does not apply them.
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runValidatorCli } from '../validator/content/cli.js'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
if (process.argv[2] === 'site') {
|
|
5
|
+
try {
|
|
6
|
+
const { runOrganizationCli } = await import('../organization/cli.mjs')
|
|
7
|
+
process.exitCode = await runOrganizationCli(process.argv.slice(3))
|
|
8
|
+
} catch (error) {
|
|
9
|
+
console.error(error.message)
|
|
10
|
+
process.exitCode = 1
|
|
11
|
+
}
|
|
12
|
+
} else process.exitCode = await runValidatorCli(process.argv.slice(2), {
|
|
5
13
|
out: value => process.stdout.write(`${value}\n`),
|
|
6
14
|
error: value => process.stderr.write(`${value}\n`)
|
|
7
15
|
})
|
package/dist/module.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { join, dirname } from 'node:path'
|
|
6
|
+
import { organizationSites, siteDirectory, checkSitePublication } from './index.mjs'
|
|
7
|
+
import { loadSiteConfig } from '../validator/content/index.js'
|
|
8
|
+
import { localizedHead, siteRoutes } from '../dist/runtime/core/index.js'
|
|
9
|
+
|
|
10
|
+
export async function runOrganizationCli(args, root = process.cwd()) {
|
|
11
|
+
const [command, key, ...extra] = args
|
|
12
|
+
if (extra.length || !['list', 'check', 'dev', 'build', 'typecheck', 'preview'].includes(command)) {
|
|
13
|
+
throw new Error('usage: nuxtjp-localized-site site <list|check|dev|build|typecheck|preview> [SITE_ID]')
|
|
14
|
+
}
|
|
15
|
+
const keys = organizationSites(root)
|
|
16
|
+
if (key !== undefined) siteDirectory(root, key)
|
|
17
|
+
if (command === 'list') { console.log(JSON.stringify(keys)); return 0 }
|
|
18
|
+
const selected = key === undefined ? keys : [key]
|
|
19
|
+
const all = keys.map(id => loadSiteConfig(siteDirectory(root, id)))
|
|
20
|
+
for (const field of ['siteId', 'origin']) {
|
|
21
|
+
assert.equal(new Set(all.map(config => config[field])).size, all.length, `Duplicate ${field}`)
|
|
22
|
+
}
|
|
23
|
+
for (const id of selected) {
|
|
24
|
+
const config = loadSiteConfig(siteDirectory(root, id))
|
|
25
|
+
const routes = siteRoutes(config)
|
|
26
|
+
assert.equal(config.externalActions, false)
|
|
27
|
+
assert.equal(config.defaultLocale, config.audienceScope === 'japan' ? 'ja' : 'en')
|
|
28
|
+
assert.equal(new Set(routes).size, routes.length)
|
|
29
|
+
assert.equal(routes.length, 8)
|
|
30
|
+
for (const route of routes) {
|
|
31
|
+
const head = localizedHead(route, config)
|
|
32
|
+
assert.ok(head.link.some(link => link.rel === 'canonical' && new URL(link.href).origin === new URL(config.origin).origin))
|
|
33
|
+
}
|
|
34
|
+
checkSitePublication(root, id)
|
|
35
|
+
if (command === 'check') console.log(JSON.stringify({ site: id, siteId: config.siteId, routes: routes.length }))
|
|
36
|
+
}
|
|
37
|
+
if (command === 'check') return 0
|
|
38
|
+
if (key === undefined) throw new Error('SITE_ID is required for Nuxt commands')
|
|
39
|
+
// Resolve the installed consumer CLI without a shell or a package-manager download.
|
|
40
|
+
const require = createRequire(join(root, 'package.json'))
|
|
41
|
+
const manifestPath = require.resolve('nuxt/package.json')
|
|
42
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
43
|
+
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin.nuxt
|
|
44
|
+
if (!bin) throw new Error('Installed Nuxt CLI not found')
|
|
45
|
+
const run = (file, args) => new Promise((resolve, reject) => {
|
|
46
|
+
const child = spawn(process.execPath, [file, ...args], {
|
|
47
|
+
cwd: root, stdio: 'inherit', env: { ...process.env, SITE_ID: key, NUXT_TELEMETRY_DISABLED: '1' }
|
|
48
|
+
})
|
|
49
|
+
child.on('error', reject)
|
|
50
|
+
child.on('exit', (code, signal) => resolve(signal ? 1 : code ?? 1))
|
|
51
|
+
})
|
|
52
|
+
if (command === 'typecheck') {
|
|
53
|
+
const prepared = await run(join(dirname(manifestPath), bin), ['prepare'])
|
|
54
|
+
if (prepared !== 0) return prepared
|
|
55
|
+
return await run(require.resolve('vue-tsc/bin/vue-tsc.js'), ['--noEmit', '--project', join(root, '.nuxt', key, 'tsconfig.json')])
|
|
56
|
+
}
|
|
57
|
+
return await run(join(dirname(manifestPath), bin), [command === 'build' ? 'generate' : command])
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function organizationSites(root: string): string[];
|
|
2
|
+
export function siteDirectory(root: string, key: string): string;
|
|
3
|
+
export function organizationSite(configUrl: string, key?: string): {
|
|
4
|
+
srcDir: string;
|
|
5
|
+
devtools: { enabled: boolean };
|
|
6
|
+
buildDir: string;
|
|
7
|
+
dir: { public: string };
|
|
8
|
+
modules: [string, { contentRoot: string }][];
|
|
9
|
+
nitro: { output: { dir: string }; prerender: { failOnError: boolean } };
|
|
10
|
+
};
|
|
11
|
+
export function checkPublicCatalog(policy: unknown, catalog: unknown): { scope: string; entries: number };
|
|
12
|
+
export function checkSitePublication(root: string, key: string): { scope: string; entries: number } | null;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import assert from 'node:assert/strict'
|
|
5
|
+
|
|
6
|
+
const keyPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
7
|
+
|
|
8
|
+
function directory(path) {
|
|
9
|
+
if (!existsSync(path) || lstatSync(path).isSymbolicLink() || !lstatSync(path).isDirectory()) {
|
|
10
|
+
throw new Error('Expected a real organization site directory')
|
|
11
|
+
}
|
|
12
|
+
return realpathSync(path)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function organizationSites(root) {
|
|
16
|
+
const sites = directory(join(directory(root), 'sites'))
|
|
17
|
+
const keys = readdirSync(sites).sort()
|
|
18
|
+
if (!keys.length) throw new Error('No organization sites configured')
|
|
19
|
+
for (const key of keys) siteDirectory(root, key)
|
|
20
|
+
return keys
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function siteDirectory(root, key) {
|
|
24
|
+
if (typeof key !== 'string' || !keyPattern.test(key)) throw new Error('Invalid SITE_ID')
|
|
25
|
+
const sites = directory(join(directory(root), 'sites'))
|
|
26
|
+
const path = directory(join(sites, key))
|
|
27
|
+
if (dirname(path) !== sites) throw new Error('Site must remain within sites/')
|
|
28
|
+
const config = join(path, 'site.config.json')
|
|
29
|
+
if (!existsSync(config) || lstatSync(config).isSymbolicLink() || !lstatSync(config).isFile()) {
|
|
30
|
+
throw new Error('Site configuration must be a regular file')
|
|
31
|
+
}
|
|
32
|
+
for (const name of ['package.json', 'nuxt.config.ts', 'app.vue', 'layouts', 'pages', 'components']) {
|
|
33
|
+
if (existsSync(join(path, name))) throw new Error('Site-specific Nuxt applications/layouts are not permitted')
|
|
34
|
+
}
|
|
35
|
+
return path
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function organizationSite(configUrl, key = process.env.SITE_ID) {
|
|
39
|
+
const root = dirname(fileURLToPath(configUrl))
|
|
40
|
+
const contentRoot = siteDirectory(root, key)
|
|
41
|
+
return {
|
|
42
|
+
srcDir: dirname(fileURLToPath(import.meta.url)),
|
|
43
|
+
devtools: { enabled: false },
|
|
44
|
+
buildDir: join(root, '.nuxt', key),
|
|
45
|
+
dir: { public: join(contentRoot, 'public') },
|
|
46
|
+
modules: [['@nuxtjp/localized-site', { contentRoot }]],
|
|
47
|
+
nitro: {
|
|
48
|
+
output: { dir: join(root, '.output', key) },
|
|
49
|
+
prerender: { failOnError: true }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function checkPublicCatalog(policy, catalog) {
|
|
55
|
+
assert.equal(policy.scope, 'public')
|
|
56
|
+
assert.ok(Array.isArray(policy.repository_ids) && policy.repository_ids.length > 0)
|
|
57
|
+
assert.equal(new Set(policy.repository_ids).size, policy.repository_ids.length)
|
|
58
|
+
assert.ok(Array.isArray(catalog.entries))
|
|
59
|
+
assert.deepEqual(catalog.entries.map(entry => entry.repository_id).sort(), [...policy.repository_ids].sort(),
|
|
60
|
+
'Catalog visibility gate failed: entries must match the reviewed public allowlist exactly')
|
|
61
|
+
return { scope: 'public', entries: catalog.entries.length }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function checkSitePublication(root, key) {
|
|
65
|
+
const path = siteDirectory(root, key)
|
|
66
|
+
const policy = join(path, 'public-package-policy.json')
|
|
67
|
+
const catalog = join(path, 'public/catalog/v2/index.json')
|
|
68
|
+
if (existsSync(policy) || existsSync(catalog)) {
|
|
69
|
+
for (const file of [policy, catalog]) {
|
|
70
|
+
if (!existsSync(file) || lstatSync(file).isSymbolicLink() || !lstatSync(file).isFile()) {
|
|
71
|
+
throw new Error('Public catalog and reviewed policy must both exist as regular files')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return checkPublicCatalog(JSON.parse(readFileSync(policy, 'utf8')), JSON.parse(readFileSync(catalog, 'utf8')))
|
|
75
|
+
}
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { organizationSites } from './index.mjs'
|
|
6
|
+
|
|
7
|
+
const [command, input] = process.argv.slice(2)
|
|
8
|
+
if (!['module', 'verify'].includes(command) || !input || process.argv.length !== 4) {
|
|
9
|
+
throw new Error('usage: workspace.mjs <module|verify> WORKSPACE_ROOT')
|
|
10
|
+
}
|
|
11
|
+
const root = resolve(input)
|
|
12
|
+
const moduleRoot = join(root, 'organizations/nuxtjp/nuxt-localized-site')
|
|
13
|
+
if (command === 'module') console.log(moduleRoot)
|
|
14
|
+
else {
|
|
15
|
+
for (const organization of readdirSync(join(root, 'organizations'), { withFileTypes: true })) {
|
|
16
|
+
if (!organization.isDirectory() || organization.isSymbolicLink()) continue
|
|
17
|
+
const owner = join(root, 'organizations', organization.name)
|
|
18
|
+
for (const entry of readdirSync(owner, { withFileTypes: true })) {
|
|
19
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue
|
|
20
|
+
const repository = join(owner, entry.name)
|
|
21
|
+
if (!existsSync(join(repository, 'sites')) || !existsSync(join(repository, 'package.json'))) continue
|
|
22
|
+
const manifest = JSON.parse(readFileSync(join(repository, 'package.json'), 'utf8'))
|
|
23
|
+
if (!manifest.dependencies?.['@nuxtjp/localized-site']) continue
|
|
24
|
+
const commands = [['test'], ...organizationSites(repository).flatMap(key => [['site', 'typecheck', key], ['site', 'build', key]])]
|
|
25
|
+
for (const args of commands) {
|
|
26
|
+
const result = spawnSync('pnpm', args, { cwd: repository, stdio: 'inherit' })
|
|
27
|
+
if (result.status !== 0) process.exit(result.status ?? 1)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuxtjp/localized-site",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Contract-first Japanese and English public-site module for Nuxt",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
|
+
"packageManager": "pnpm@10.29.3",
|
|
7
8
|
"engines": {
|
|
8
9
|
"node": "^22.19.0 || ^24.11.0",
|
|
9
10
|
"pnpm": ">=10 <11"
|
|
@@ -18,7 +19,11 @@
|
|
|
18
19
|
"README.md",
|
|
19
20
|
"NOTICE",
|
|
20
21
|
"LICENSE-ASSETS",
|
|
21
|
-
"LICENSE-PREVIOUS"
|
|
22
|
+
"LICENSE-PREVIOUS",
|
|
23
|
+
"README.ja.md",
|
|
24
|
+
"organization",
|
|
25
|
+
"security",
|
|
26
|
+
"ASSET-SCOPE.md"
|
|
22
27
|
],
|
|
23
28
|
"bin": {
|
|
24
29
|
"nuxtjp-localized-site": "./bin/nuxtjp-localized-site.mjs"
|
|
@@ -38,20 +43,36 @@
|
|
|
38
43
|
"types": "./validator/content/index.d.ts",
|
|
39
44
|
"import": "./validator/content/index.js"
|
|
40
45
|
},
|
|
41
|
-
"./schema": "./schemas/localized-site-v1.schema.json"
|
|
46
|
+
"./schema": "./schemas/localized-site-v1.schema.json",
|
|
47
|
+
"./organization": {
|
|
48
|
+
"types": "./organization/index.d.mts",
|
|
49
|
+
"import": "./organization/index.mjs"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build:module": "nuxt-module-build build",
|
|
54
|
+
"build:validator": "tsc -p tsconfig.validator.json",
|
|
55
|
+
"build": "pnpm run build:validator && pnpm run build:module && nuxt build playground",
|
|
56
|
+
"typecheck": "vue-tsc --noEmit --project tsconfig.json",
|
|
57
|
+
"test": "vitest run && pnpm run test:organization && pnpm run test:dependency-security && pnpm run test:consumer-security",
|
|
58
|
+
"validate:example": "node ./bin/nuxtjp-localized-site.mjs validate --content-root ./examples/content",
|
|
59
|
+
"prepack": "pnpm test && pnpm typecheck && pnpm build && pnpm validate:example",
|
|
60
|
+
"test:organization": "pnpm run build:validator && pnpm run build:module && node --test test/organization-check.mjs",
|
|
61
|
+
"test:dependency-security": "node --test scripts/dependency-security.check.cjs",
|
|
62
|
+
"test:consumer-security": "node --test scripts/consumer-security.check.mjs"
|
|
42
63
|
},
|
|
43
64
|
"peerDependencies": {
|
|
44
65
|
"nuxt": "^4.5.1",
|
|
45
66
|
"vue": "^3.5.40"
|
|
46
67
|
},
|
|
47
68
|
"dependencies": {
|
|
48
|
-
"@nuxt/kit": "4.5.
|
|
69
|
+
"@nuxt/kit": "4.5.2",
|
|
49
70
|
"@nuxt/schema": "4.5.1"
|
|
50
71
|
},
|
|
51
72
|
"devDependencies": {
|
|
52
73
|
"@nuxt/module-builder": "1.0.2",
|
|
53
74
|
"@types/node": "24.12.0",
|
|
54
|
-
"nuxt": "4.5.
|
|
75
|
+
"nuxt": "4.5.2",
|
|
55
76
|
"typescript": "5.9.3",
|
|
56
77
|
"vitest": "4.1.11",
|
|
57
78
|
"vue": "3.5.43",
|
|
@@ -66,12 +87,13 @@
|
|
|
66
87
|
"access": "public",
|
|
67
88
|
"provenance": true
|
|
68
89
|
},
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
"pnpm": {
|
|
91
|
+
"overrides": {
|
|
92
|
+
"@nuxt/devtools>simple-git": "4.0.2"
|
|
93
|
+
},
|
|
94
|
+
"patchedDependencies": {
|
|
95
|
+
"braces@3.0.3": "patches/braces@3.0.3.patch",
|
|
96
|
+
"node-forge@1.4.0": "patches/node-forge@1.4.0.patch"
|
|
97
|
+
}
|
|
76
98
|
}
|
|
77
|
-
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Dependency security backports
|
|
2
|
+
|
|
3
|
+
Two upstream dependencies have no fixed release: braces 3.0.3 (GHSA-vfj7-8cjw-p6xm) and node-forge 1.4.0 (GHSA-86w9-cpqp-85rv). The included patches bound brace AST recursion and reject malformed RSA DigestAlgorithm parameters. Regression tests cover ordinary behavior and the malicious input shapes.
|
|
4
|
+
|
|
5
|
+
The package is distributed through npm. A consumer must use pnpm 10 and explicitly configure the reviewed backports at its project root. Dependency packages cannot impose root-level npm overrides or pnpm patches on a consuming application. Installing this module alone does not apply the patches.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
node node_modules/@nuxtjp/localized-site/security/apply.mjs --project-root . --apply
|
|
9
|
+
pnpm install --no-frozen-lockfile
|
|
10
|
+
pnpm install --frozen-lockfile
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Commit the generated `.nuxtjp-security/*.patch`, `package.json`, and lockfile in your application. The command does not run installs, access the network, or replace an existing conflicting patch. It preserves unrelated pnpm settings. Each root application must apply the patches, including CI checkouts.
|
|
14
|
+
|
|
15
|
+
Run `node node_modules/@nuxtjp/localized-site/security/dependency-security.check.cjs` after installing the locked project to verify that the actual resolved dependencies reject the affected inputs. These are backports, not upstream fixed versions. Version-only advisory tools still report the two vulnerable upstream versions; retain their findings and link the lockfile/patch hashes and regression results. Reapply and reverify when dependency versions change. npm-only consumers require a separately reviewed override distribution; they are not certified by this pnpm procedure.
|
|
16
|
+
|
|
17
|
+
The RSA test creates an ephemeral key in memory and never reads a user key. Patches contain code only and do not include credentials, customer content, or private endpoints.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** Apply reviewed Nuxt dependency backports to an explicitly selected pnpm project. */
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
|
+
import { applySecurityPatches } from './project.mjs'
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2)
|
|
8
|
+
if (args.length !== 3 || args[0] !== '--project-root' || args[2] !== '--apply') {
|
|
9
|
+
console.error('Usage: node apply.mjs --project-root PROJECT --apply')
|
|
10
|
+
process.exitCode = 2
|
|
11
|
+
} else {
|
|
12
|
+
try {
|
|
13
|
+
applySecurityPatches(args[1], dirname(fileURLToPath(import.meta.url)))
|
|
14
|
+
console.log('Reviewed backports configured. Run pnpm install --no-frozen-lockfile to refresh the lockfile.')
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error(error.message)
|
|
17
|
+
process.exitCode = 1
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { test } = require('node:test');
|
|
3
|
+
const { createRequire } = require('node:module');
|
|
4
|
+
const { generateKeyPairSync, sign, privateEncrypt, constants } = require('node:crypto');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
function dependency(name, version) {
|
|
8
|
+
const explicit = process.env['SECURITY_TEST_' + name.toUpperCase().replace('-', '_')];
|
|
9
|
+
if (explicit) return require(path.resolve(explicit));
|
|
10
|
+
const { installedDependencies } = require('./resolved-dependency.cjs');
|
|
11
|
+
const matches = installedDependencies(process.cwd(), name, version);
|
|
12
|
+
assert.equal(matches.length, 1, 'Expected one active locked dependency implementation');
|
|
13
|
+
return createRequire(matches[0])(name);
|
|
14
|
+
}
|
|
15
|
+
const braces = dependency('braces', '3.0.3');
|
|
16
|
+
const forge = dependency('node-forge', '1.4.0');
|
|
17
|
+
test('brace compilation, expansion and stringification retain ordinary behavior', () => {
|
|
18
|
+
assert.equal(braces.compile('src/{a,b}.vue'), 'src/(a|b).vue');
|
|
19
|
+
assert.deepEqual(braces.expand('src/{a,b}.vue'), ['src/a.vue', 'src/b.vue']);
|
|
20
|
+
assert.equal(braces.stringify(braces.parse('src/{a,b}.vue')), 'src/{a,b}.vue');
|
|
21
|
+
assert.equal(braces.compile('src/\\{a,b\\}.vue'), 'src/{a,b}.vue');
|
|
22
|
+
});
|
|
23
|
+
test('deep brace, parenthesis and caller AST inputs reject before stack exhaustion', () => {
|
|
24
|
+
for (const pattern of ['{'.repeat(6000) + 'a,b' + '}'.repeat(6000), '('.repeat(6000) + 'x' + ')'.repeat(6000), '{'.repeat(6000) + 'x']) {
|
|
25
|
+
for (const fn of [braces.compile, braces.expand, braces.stringify, braces.parse]) assert.throws(() => fn(pattern), SyntaxError);
|
|
26
|
+
}
|
|
27
|
+
let ast = { type: 'text', value: 'x' };
|
|
28
|
+
for (let i = 0; i < 6000; i++) ast = { type: 'root', nodes: [ast] };
|
|
29
|
+
for (const fn of [braces.compile, braces.expand, braces.stringify]) assert.throws(() => fn(ast), SyntaxError);
|
|
30
|
+
const cyclic = { type: 'root', nodes: [] }; cyclic.nodes.push(cyclic);
|
|
31
|
+
assert.throws(() => braces.compile(cyclic), SyntaxError);
|
|
32
|
+
});
|
|
33
|
+
test('RSA verifier accepts standard signatures and rejects extra or malformed DigestAlgorithm elements', () => {
|
|
34
|
+
const keys = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
|
35
|
+
const publicKey = forge.pki.publicKeyFromPem(keys.publicKey.export({ type: 'spki', format: 'pem' }));
|
|
36
|
+
const data = Buffer.from('never-issued security regression fixture');
|
|
37
|
+
const md = forge.md.sha256.create(); md.update(data.toString('binary'));
|
|
38
|
+
const digest = md.digest().getBytes();
|
|
39
|
+
assert.equal(publicKey.verify(digest, sign('sha256', data, keys.privateKey).toString('binary')), true);
|
|
40
|
+
const a = forge.asn1;
|
|
41
|
+
const oid = () => a.create(a.Class.UNIVERSAL, a.Type.OID, false, a.oidToDer(forge.oids.sha256).getBytes());
|
|
42
|
+
const nil = value => a.create(a.Class.UNIVERSAL, a.Type.NULL, false, value);
|
|
43
|
+
for (const parameters of [[oid(), nil(''), oid()], [oid(), a.create(a.Class.UNIVERSAL, a.Type.SEQUENCE, true, [oid()])], [oid(), nil('x')]]) {
|
|
44
|
+
const info = a.create(a.Class.UNIVERSAL, a.Type.SEQUENCE, true, [a.create(a.Class.UNIVERSAL, a.Type.SEQUENCE, true, parameters), a.create(a.Class.UNIVERSAL, a.Type.OCTETSTRING, false, digest)]);
|
|
45
|
+
const encoded = Buffer.from(a.toDer(info).getBytes(), 'binary');
|
|
46
|
+
const signature = privateEncrypt({ key: keys.privateKey, padding: constants.RSA_PKCS1_PADDING }, encoded).toString('binary');
|
|
47
|
+
let accepted = false;
|
|
48
|
+
try { accepted = publicKey.verify(digest, signature); } catch { /* Rejection is the expected result for a malformed signature. */ }
|
|
49
|
+
assert.equal(accepted, false, 'Malformed algorithm must not verify');
|
|
50
|
+
}
|
|
51
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
diff --git a/lib/depth-guard.js b/lib/depth-guard.js
|
|
2
|
+
new file mode 100644
|
|
3
|
+
--- /dev/null
|
|
4
|
+
+++ b/lib/depth-guard.js
|
|
5
|
+
@@ -0,0 +1,19 @@
|
|
6
|
+
+'use strict';
|
|
7
|
+
+
|
|
8
|
+
+// Wonderland security backport: bound recursive walkers, including caller ASTs.
|
|
9
|
+
+module.exports = ast => {
|
|
10
|
+
+ const pending = [[ast, 0]];
|
|
11
|
+
+ const seen = new Set();
|
|
12
|
+
+ let count = 0;
|
|
13
|
+
+ while (pending.length) {
|
|
14
|
+
+ const [node, depth] = pending.pop();
|
|
15
|
+
+ if (depth > 64 || ++count > 100000 || seen.has(node)) {
|
|
16
|
+
+ throw new SyntaxError('Brace pattern exceeds safe AST bounds');
|
|
17
|
+
+ }
|
|
18
|
+
+ if (!node || typeof node !== 'object') continue;
|
|
19
|
+
+ seen.add(node);
|
|
20
|
+
+ if (Array.isArray(node.nodes)) {
|
|
21
|
+
+ for (const child of node.nodes) pending.push([child, depth + 1]);
|
|
22
|
+
+ }
|
|
23
|
+
+ }
|
|
24
|
+
+};
|
|
25
|
+
diff --git a/lib/parse.js b/lib/parse.js
|
|
26
|
+
--- a/lib/parse.js
|
|
27
|
+
+++ b/lib/parse.js
|
|
28
|
+
@@ -144,6 +144,7 @@
|
|
29
|
+
|
|
30
|
+
if (value === CHAR_LEFT_PARENTHESES) {
|
|
31
|
+
block = push({ type: 'paren', nodes: [] });
|
|
32
|
+
+ if (stack.length >= 64) throw new SyntaxError('Brace pattern exceeds safe nesting depth');
|
|
33
|
+
stack.push(block);
|
|
34
|
+
push({ type: 'text', value });
|
|
35
|
+
continue;
|
|
36
|
+
@@ -210,6 +211,7 @@
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
block = push(brace);
|
|
40
|
+
+ if (stack.length >= 64) throw new SyntaxError('Brace pattern exceeds safe nesting depth');
|
|
41
|
+
stack.push(block);
|
|
42
|
+
push({ type: 'open', value });
|
|
43
|
+
continue;
|
|
44
|
+
diff --git a/lib/compile.js b/lib/compile.js
|
|
45
|
+
--- a/lib/compile.js
|
|
46
|
+
+++ b/lib/compile.js
|
|
47
|
+
@@ -1,9 +1,12 @@
|
|
48
|
+
'use strict';
|
|
49
|
+
+
|
|
50
|
+
+const assertDepth = require('./depth-guard');
|
|
51
|
+
|
|
52
|
+
const fill = require('fill-range');
|
|
53
|
+
const utils = require('./utils');
|
|
54
|
+
|
|
55
|
+
const compile = (ast, options = {}) => {
|
|
56
|
+
+ assertDepth(ast);
|
|
57
|
+
const walk = (node, parent = {}) => {
|
|
58
|
+
const invalidBlock = utils.isInvalidBrace(parent);
|
|
59
|
+
const invalidNode = node.invalid === true && options.escapeInvalid === true;
|
|
60
|
+
diff --git a/lib/expand.js b/lib/expand.js
|
|
61
|
+
--- a/lib/expand.js
|
|
62
|
+
+++ b/lib/expand.js
|
|
63
|
+
@@ -1,4 +1,6 @@
|
|
64
|
+
'use strict';
|
|
65
|
+
+
|
|
66
|
+
+const assertDepth = require('./depth-guard');
|
|
67
|
+
|
|
68
|
+
const fill = require('fill-range');
|
|
69
|
+
const stringify = require('./stringify');
|
|
70
|
+
@@ -31,6 +33,7 @@
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const expand = (ast, options = {}) => {
|
|
74
|
+
+ assertDepth(ast);
|
|
75
|
+
const rangeLimit = options.rangeLimit === undefined ? 1000 : options.rangeLimit;
|
|
76
|
+
|
|
77
|
+
const walk = (node, parent = {}) => {
|
|
78
|
+
diff --git a/lib/stringify.js b/lib/stringify.js
|
|
79
|
+
--- a/lib/stringify.js
|
|
80
|
+
+++ b/lib/stringify.js
|
|
81
|
+
@@ -1,8 +1,11 @@
|
|
82
|
+
'use strict';
|
|
83
|
+
+
|
|
84
|
+
+const assertDepth = require('./depth-guard');
|
|
85
|
+
|
|
86
|
+
const utils = require('./utils');
|
|
87
|
+
|
|
88
|
+
module.exports = (ast, options = {}) => {
|
|
89
|
+
+ assertDepth(ast);
|
|
90
|
+
const stringify = (node, parent = {}) => {
|
|
91
|
+
const invalidBlock = options.escapeInvalid && utils.isInvalidBrace(parent);
|
|
92
|
+
const invalidNode = node.invalid === true && options.escapeInvalid === true;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
diff --git a/lib/rsa.js b/lib/rsa.js
|
|
2
|
+
--- a/lib/rsa.js
|
|
3
|
+
+++ b/lib/rsa.js
|
|
4
|
+
@@ -1172,7 +1172,15 @@
|
|
5
|
+
var capture = {};
|
|
6
|
+
var errors = [];
|
|
7
|
+
if(!asn1.validate(obj, digestInfoValidator, capture, errors) ||
|
|
8
|
+
- obj.value.length !== 2) {
|
|
9
|
+
+ obj.value.length !== 2 ||
|
|
10
|
+
+ // Wonderland security backport: AlgorithmIdentifier has one OID
|
|
11
|
+
+ // and, optionally, exactly one empty primitive NULL parameter.
|
|
12
|
+
+ obj.value[0].value.length < 1 || obj.value[0].value.length > 2 ||
|
|
13
|
+
+ (obj.value[0].value.length === 2 &&
|
|
14
|
+
+ (obj.value[0].value[1].tagClass !== asn1.Class.UNIVERSAL ||
|
|
15
|
+
+ obj.value[0].value[1].type !== asn1.Type.NULL ||
|
|
16
|
+
+ obj.value[0].value[1].constructed !== false ||
|
|
17
|
+
+ obj.value[0].value[1].value !== ''))) {
|
|
18
|
+
var error = new Error(
|
|
19
|
+
'ASN.1 object does not contain a valid RSASSA-PKCS1-v1_5 ' +
|
|
20
|
+
'DigestInfo value.');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Configure caller-owned pnpm backports without executing installs or changing unrelated settings. */
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
import { randomUUID } from 'node:crypto'
|
|
5
|
+
|
|
6
|
+
const patches = ['braces@3.0.3', 'node-forge@1.4.0']
|
|
7
|
+
function regular(path) {
|
|
8
|
+
const stat = lstatSync(path)
|
|
9
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 1024 * 1024) {
|
|
10
|
+
throw new Error('Expected a bounded regular project or patch file')
|
|
11
|
+
}
|
|
12
|
+
return readFileSync(path)
|
|
13
|
+
}
|
|
14
|
+
function object(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reject conflicting patches and links before mutating a project. No network access occurs. */
|
|
19
|
+
export function applySecurityPatches(project, source) {
|
|
20
|
+
const root = realpathSync(resolve(project))
|
|
21
|
+
const manifest = join(root, 'package.json')
|
|
22
|
+
const original = regular(manifest)
|
|
23
|
+
const data = JSON.parse(original)
|
|
24
|
+
if (!object(data) || (data.pnpm !== undefined && !object(data.pnpm))) {
|
|
25
|
+
throw new Error('Expected a package manifest with object-valued pnpm settings')
|
|
26
|
+
}
|
|
27
|
+
const pnpm = data.pnpm ?? {}
|
|
28
|
+
const existing = pnpm.patchedDependencies ?? {}
|
|
29
|
+
if (!object(existing)) throw new Error('Expected object-valued patchedDependencies')
|
|
30
|
+
const dir = join(root, '.nuxtjp-security')
|
|
31
|
+
if (existsSync(dir) && (lstatSync(dir).isSymbolicLink() || !lstatSync(dir).isDirectory())) {
|
|
32
|
+
throw new Error('Security patch directory must be a real directory')
|
|
33
|
+
}
|
|
34
|
+
const inputs = patches.map(name => {
|
|
35
|
+
const relative = `.nuxtjp-security/${name}.patch`
|
|
36
|
+
if (existing[name] !== undefined && existing[name] !== relative) {
|
|
37
|
+
throw new Error('An existing dependency patch conflicts; review it before applying')
|
|
38
|
+
}
|
|
39
|
+
const bytes = regular(join(source, 'patches', `${name}.patch`))
|
|
40
|
+
const target = join(root, relative)
|
|
41
|
+
if (existsSync(target) && !regular(target).equals(bytes)) {
|
|
42
|
+
throw new Error('An existing patch differs; no files were overwritten')
|
|
43
|
+
}
|
|
44
|
+
return { name, relative, bytes, target }
|
|
45
|
+
})
|
|
46
|
+
if (!readFileSync(manifest).equals(original)) throw new Error('Manifest changed during preparation')
|
|
47
|
+
mkdirSync(dir, { recursive: true })
|
|
48
|
+
for (const patch of inputs) {
|
|
49
|
+
if (!existsSync(patch.target)) writeFileSync(patch.target, patch.bytes, { flag: 'wx' })
|
|
50
|
+
existing[patch.name] = patch.relative
|
|
51
|
+
}
|
|
52
|
+
data.pnpm = { ...pnpm, patchedDependencies: existing }
|
|
53
|
+
const temporary = join(root, `.nuxtjp-manifest-${randomUUID()}.tmp`)
|
|
54
|
+
try {
|
|
55
|
+
writeFileSync(temporary, JSON.stringify(data, null, 2) + '\n', { flag: 'wx' })
|
|
56
|
+
if (!readFileSync(manifest).equals(original)) throw new Error('Manifest changed; retry after review')
|
|
57
|
+
renameSync(temporary, manifest)
|
|
58
|
+
} finally {
|
|
59
|
+
if (existsSync(temporary)) unlinkSync(temporary)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Resolve a dependency through the installed application graph, ignoring unused store entries. */
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { createRequire } = require('node:module');
|
|
5
|
+
|
|
6
|
+
function installedDependencies(root, target, version) {
|
|
7
|
+
root = fs.realpathSync(root);
|
|
8
|
+
const pending = [path.join(root, 'package.json')];
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
const found = new Set();
|
|
11
|
+
while (pending.length) {
|
|
12
|
+
if (seen.size > 5000) throw new Error('Installed dependency graph exceeds verification budget');
|
|
13
|
+
const file = pending.pop();
|
|
14
|
+
if (seen.has(file)) continue;
|
|
15
|
+
seen.add(file);
|
|
16
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
if (data.name === target) {
|
|
18
|
+
if (data.version !== version) throw new Error('Dependency version changed; review its security patch');
|
|
19
|
+
found.add(file);
|
|
20
|
+
}
|
|
21
|
+
const request = createRequire(file);
|
|
22
|
+
const names = Object.keys({ ...data.dependencies, ...data.optionalDependencies, ...data.peerDependencies,
|
|
23
|
+
...(file === path.join(root, 'package.json') ? data.devDependencies : {}) });
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
for (const location of request.resolve.paths(name) ?? []) {
|
|
26
|
+
const candidate = path.join(location, name, 'package.json');
|
|
27
|
+
if (!fs.existsSync(candidate)) continue;
|
|
28
|
+
const real = fs.realpathSync(candidate);
|
|
29
|
+
if (!real.startsWith(path.join(root, 'node_modules') + path.sep)) {
|
|
30
|
+
throw new Error('Dependency resolves outside the selected project; select the workspace root');
|
|
31
|
+
}
|
|
32
|
+
pending.push(real);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (found.size === 0) throw new Error('Dependency not present in the installed application graph');
|
|
38
|
+
return [...found].sort();
|
|
39
|
+
}
|
|
40
|
+
module.exports = { installedDependencies };
|