@hagicode/hagiscript 0.2.8-dev.128.1.2ae1976 → 0.2.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hagicode/hagiscript",
3
- "version": "0.2.8-dev.128.1.2ae1976",
3
+ "version": "0.2.8",
4
4
  "description": "Scoped npm package foundation for Hagiscript language tooling.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/HagiCode-org/hagiscript#readme",
@@ -37,7 +37,7 @@
37
37
  "format:check": "prettier --check .",
38
38
  "lint": "eslint .",
39
39
  "integration:installed-runtime": "node scripts/integration-installed-runtime.mjs",
40
- "integration:runtime-key-path": "node scripts/integration-runtime-key-path.mjs",
40
+ "integration:runtime-key-path": "npm run build && node scripts/integration-runtime-key-path.mjs",
41
41
  "integration:runtime-management": "npm run build && node scripts/integration-runtime-management.mjs",
42
42
  "playground:manifest:generate": "node scripts/generate-playground-manifest.mjs",
43
43
  "playground:runtime:install": "npm run playground:manifest:generate && npm run dev -- runtime install --from-manifest ./playground/generated/manifest.yaml --runtime-root ./playground/runtime-root",
@@ -4,6 +4,7 @@ import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, rename, rm, stat,
4
4
  import { homedir, tmpdir } from "node:os"
5
5
  import path from "node:path"
6
6
  import process from "node:process"
7
+ import { URL } from "node:url"
7
8
  import { createGunzip } from "node:zlib"
8
9
  import { pipeline } from "node:stream/promises"
9
10
  import { extractZipArchive as extractZipArchiveWithNode } from "./zip-extract.mjs"
@@ -188,31 +189,26 @@ export async function installVendoredPackage(context, options) {
188
189
  context.vendoredRepository,
189
190
  context.vendoredBaseUrl
190
191
  )
191
- const releaseTag = context.vendoredTag
192
192
  const platform = normalizeVendoredPlatform(process.platform)
193
193
  const arch = normalizeVendoredArchitecture(process.arch)
194
194
  const installMode = options.installMode || context.bundledInstallMode || "extract"
195
195
  const archiveFormat = resolveVendoredArchiveFormat(platform, installMode)
196
- const assetName = buildVendoredAssetName({
196
+ const preferredSource = createVendoredAssetSource({
197
+ repository,
198
+ baseUrl,
199
+ releaseTag: context.vendoredTag,
197
200
  packageName: options.packageName,
198
- releaseTag,
199
201
  platform,
200
202
  arch,
201
- archiveFormat
203
+ archiveFormat,
204
+ downloadCacheDir: context.downloadCacheDir
202
205
  })
203
- const assetUrl = buildVendoredAssetUrl(baseUrl, repository, releaseTag, assetName)
204
- const cacheRoot = path.join(
205
- context.downloadCacheDir,
206
- "vendored",
207
- options.packageName,
208
- releaseTag,
209
- `${platform}-${arch}`
210
- )
211
206
 
212
207
  if (installMode === "archive-7z-only") {
213
208
  const archivePath =
214
- options.archivePath || path.join(path.dirname(options.prefixRoot), "archives", assetName)
215
- const archiveCachePath = path.join(cacheRoot, assetName)
209
+ options.archivePath || path.join(path.dirname(options.prefixRoot), "archives", preferredSource.assetName)
210
+ let resolvedSource = preferredSource
211
+ let archiveCachePath = path.join(resolvedSource.cacheRoot, resolvedSource.assetName)
216
212
 
217
213
  if (context.downloadCacheEnabled) {
218
214
  const restoredArchivePath = await restoreVendoredArchiveFromCache(
@@ -220,91 +216,134 @@ export async function installVendoredPackage(context, options) {
220
216
  archivePath
221
217
  )
222
218
  if (restoredArchivePath) {
223
- return {
219
+ return buildVendoredInstallResult(resolvedSource, {
224
220
  installMode,
225
221
  archivePath: restoredArchivePath,
226
- archiveFormat,
227
- releaseRepository: repository,
228
- releaseTag,
229
- releaseName: releaseTag.replace(/^v/u, ""),
230
- releaseUrl: `${baseUrl}/${repository}/releases/tag/${encodeURIComponent(releaseTag)}`,
231
- releaseAssetName: assetName,
232
- releaseAssetUrl: assetUrl
233
- }
222
+ archiveFormat
223
+ })
234
224
  }
235
225
  }
236
226
 
237
227
  await ensureDirectory(path.dirname(archivePath))
238
- await downloadVendoredAsset(assetUrl, archivePath)
228
+
229
+ try {
230
+ await downloadVendoredAsset(resolvedSource.assetUrl, archivePath)
231
+ } catch (error) {
232
+ const fallbackSource = await resolveFallbackVendoredAssetSource(
233
+ preferredSource,
234
+ error
235
+ )
236
+
237
+ if (!fallbackSource) {
238
+ throw error
239
+ }
240
+
241
+ resolvedSource = fallbackSource
242
+ archiveCachePath = path.join(resolvedSource.cacheRoot, resolvedSource.assetName)
243
+
244
+ if (context.downloadCacheEnabled) {
245
+ const restoredArchivePath = await restoreVendoredArchiveFromCache(
246
+ archiveCachePath,
247
+ archivePath
248
+ )
249
+ if (restoredArchivePath) {
250
+ return buildVendoredInstallResult(resolvedSource, {
251
+ installMode,
252
+ archivePath: restoredArchivePath,
253
+ archiveFormat
254
+ })
255
+ }
256
+ }
257
+
258
+ await downloadVendoredAsset(resolvedSource.assetUrl, archivePath)
259
+ }
239
260
 
240
261
  if (context.downloadCacheEnabled) {
241
262
  await storeFileInCache(archivePath, archiveCachePath)
242
263
  }
243
264
 
244
- return {
265
+ return buildVendoredInstallResult(resolvedSource, {
245
266
  installMode,
246
267
  archivePath,
247
- archiveFormat,
248
- releaseRepository: repository,
249
- releaseTag,
250
- releaseName: releaseTag.replace(/^v/u, ""),
251
- releaseUrl: `${baseUrl}/${repository}/releases/tag/${encodeURIComponent(releaseTag)}`,
252
- releaseAssetName: assetName,
253
- releaseAssetUrl: assetUrl
254
- }
268
+ archiveFormat
269
+ })
255
270
  }
256
271
 
257
272
  if (context.downloadCacheEnabled) {
258
273
  const restoredEntrypoint = await restoreVendoredPackageFromCache(
259
- cacheRoot,
274
+ preferredSource.cacheRoot,
260
275
  options.prefixRoot,
261
276
  options.entrypointRelativePath
262
277
  )
263
278
  if (restoredEntrypoint) {
264
- return {
279
+ return buildVendoredInstallResult(preferredSource, {
265
280
  installMode,
266
281
  entrypointPath: restoredEntrypoint,
267
- archiveFormat,
268
- releaseRepository: repository,
269
- releaseTag,
270
- releaseName: releaseTag.replace(/^v/u, ""),
271
- releaseUrl: `${baseUrl}/${repository}/releases/tag/${encodeURIComponent(releaseTag)}`,
272
- releaseAssetName: assetName,
273
- releaseAssetUrl: assetUrl
274
- }
282
+ archiveFormat
283
+ })
275
284
  }
276
285
  }
277
286
 
278
287
  const stagingRoot = await mkdtemp(path.join(tmpdir(), `hagiscript-vendored-${options.packageName}-`))
279
288
 
280
289
  try {
281
- const archivePath = path.join(stagingRoot, assetName)
282
290
  const extractRoot = path.join(stagingRoot, "extract")
283
- await downloadVendoredAsset(assetUrl, archivePath)
284
- const extractedRoot = await extractVendoredArchive(
285
- archivePath,
286
- extractRoot,
287
- path.extname(assetName).toLowerCase() === ".zip" ? "zip" : "tar.gz"
288
- )
291
+ let resolvedSource = preferredSource
292
+ let extractedRoot
293
+
294
+ try {
295
+ extractedRoot = await downloadAndExtractVendoredPackage(
296
+ resolvedSource,
297
+ stagingRoot,
298
+ extractRoot
299
+ )
300
+ } catch (error) {
301
+ const fallbackSource = await resolveFallbackVendoredAssetSource(
302
+ preferredSource,
303
+ error
304
+ )
305
+
306
+ if (!fallbackSource) {
307
+ throw error
308
+ }
309
+
310
+ resolvedSource = fallbackSource
311
+
312
+ if (context.downloadCacheEnabled) {
313
+ const restoredEntrypoint = await restoreVendoredPackageFromCache(
314
+ resolvedSource.cacheRoot,
315
+ options.prefixRoot,
316
+ options.entrypointRelativePath
317
+ )
318
+ if (restoredEntrypoint) {
319
+ return buildVendoredInstallResult(resolvedSource, {
320
+ installMode,
321
+ entrypointPath: restoredEntrypoint,
322
+ archiveFormat
323
+ })
324
+ }
325
+ }
326
+
327
+ extractedRoot = await downloadAndExtractVendoredPackage(
328
+ resolvedSource,
329
+ stagingRoot,
330
+ extractRoot
331
+ )
332
+ }
333
+
289
334
  await replaceDirectory(extractedRoot, options.prefixRoot)
290
335
  if (context.downloadCacheEnabled) {
291
- await storeDirectoryInCache(options.prefixRoot, cacheRoot)
336
+ await storeDirectoryInCache(options.prefixRoot, resolvedSource.cacheRoot)
292
337
  }
293
338
 
294
339
  const entrypointPath = path.join(options.prefixRoot, options.entrypointRelativePath)
295
340
  await access(entrypointPath)
296
341
 
297
- return {
342
+ return buildVendoredInstallResult(resolvedSource, {
298
343
  installMode,
299
344
  entrypointPath,
300
- archiveFormat,
301
- releaseRepository: repository,
302
- releaseTag,
303
- releaseName: releaseTag.replace(/^v/u, ""),
304
- releaseUrl: `${baseUrl}/${repository}/releases/tag/${encodeURIComponent(releaseTag)}`,
305
- releaseAssetName: assetName,
306
- releaseAssetUrl: assetUrl
307
- }
345
+ archiveFormat
346
+ })
308
347
  } finally {
309
348
  await rm(stagingRoot, { recursive: true, force: true })
310
349
  }
@@ -469,6 +508,63 @@ function buildVendoredAssetUrl(baseUrl, repository, releaseTag, assetName) {
469
508
  return `${baseUrl}/${repository}/releases/download/${encodeURIComponent(releaseTag)}/${encodeURIComponent(assetName)}`
470
509
  }
471
510
 
511
+ function buildVendoredReleaseUrl(baseUrl, repository, releaseTag) {
512
+ return `${baseUrl}/${repository}/releases/tag/${encodeURIComponent(releaseTag)}`
513
+ }
514
+
515
+ function createVendoredAssetSource(options) {
516
+ const assetName = buildVendoredAssetName({
517
+ packageName: options.packageName,
518
+ releaseTag: options.releaseTag,
519
+ platform: options.platform,
520
+ arch: options.arch,
521
+ archiveFormat: options.archiveFormat
522
+ })
523
+
524
+ return {
525
+ repository: options.repository,
526
+ baseUrl: options.baseUrl,
527
+ releaseTag: options.releaseTag,
528
+ releaseName: options.releaseTag.replace(/^v/u, ""),
529
+ releaseUrl: buildVendoredReleaseUrl(
530
+ options.baseUrl,
531
+ options.repository,
532
+ options.releaseTag
533
+ ),
534
+ packageName: options.packageName,
535
+ platform: options.platform,
536
+ arch: options.arch,
537
+ archiveFormat: options.archiveFormat,
538
+ assetName,
539
+ assetUrl: buildVendoredAssetUrl(
540
+ options.baseUrl,
541
+ options.repository,
542
+ options.releaseTag,
543
+ assetName
544
+ ),
545
+ downloadCacheDir: options.downloadCacheDir,
546
+ cacheRoot: path.join(
547
+ options.downloadCacheDir,
548
+ "vendored",
549
+ options.packageName,
550
+ options.releaseTag,
551
+ `${options.platform}-${options.arch}`
552
+ )
553
+ }
554
+ }
555
+
556
+ function buildVendoredInstallResult(source, extra = {}) {
557
+ return {
558
+ releaseRepository: source.repository,
559
+ releaseTag: source.releaseTag,
560
+ releaseName: source.releaseName,
561
+ releaseUrl: source.releaseUrl,
562
+ releaseAssetName: source.assetName,
563
+ releaseAssetUrl: source.assetUrl,
564
+ ...extra
565
+ }
566
+ }
567
+
472
568
  function normalizeVendoredReleaseVersion(releaseTag) {
473
569
  const normalized = String(releaseTag || "").trim().replace(/^v/u, "")
474
570
  if (!normalized) {
@@ -556,6 +652,113 @@ async function downloadVendoredAsset(url, destinationPath) {
556
652
  }
557
653
  }
558
654
 
655
+ async function downloadAndExtractVendoredPackage(source, stagingRoot, extractRoot) {
656
+ const archivePath = path.join(stagingRoot, source.assetName)
657
+ await downloadVendoredAsset(source.assetUrl, archivePath)
658
+ return await extractVendoredArchive(
659
+ archivePath,
660
+ extractRoot,
661
+ source.archiveFormat === "zip" ? "zip" : "tar.gz"
662
+ )
663
+ }
664
+
665
+ async function resolveFallbackVendoredAssetSource(preferredSource, error) {
666
+ if (!isVendoredAssetMissingError(error)) {
667
+ return null
668
+ }
669
+
670
+ const releases = await listVendoredReleases(
671
+ preferredSource.baseUrl,
672
+ preferredSource.repository
673
+ )
674
+
675
+ for (const release of releases) {
676
+ const releaseTag = typeof release?.tag_name === "string" ? release.tag_name.trim() : ""
677
+ if (!releaseTag || releaseTag === preferredSource.releaseTag) {
678
+ continue
679
+ }
680
+
681
+ const candidate = createVendoredAssetSource({
682
+ repository: preferredSource.repository,
683
+ baseUrl: preferredSource.baseUrl,
684
+ releaseTag,
685
+ packageName: preferredSource.packageName,
686
+ platform: preferredSource.platform,
687
+ arch: preferredSource.arch,
688
+ archiveFormat: preferredSource.archiveFormat,
689
+ downloadCacheDir: preferredSource.downloadCacheDir
690
+ })
691
+ const assetNames = Array.isArray(release.assets)
692
+ ? new Set(
693
+ release.assets
694
+ .map((asset) => (typeof asset?.name === "string" ? asset.name : ""))
695
+ .filter(Boolean)
696
+ )
697
+ : new Set()
698
+
699
+ if (assetNames.has(candidate.assetName)) {
700
+ return candidate
701
+ }
702
+ }
703
+
704
+ return null
705
+ }
706
+
707
+ async function listVendoredReleases(baseUrl, repository) {
708
+ const response = await globalThis.fetch(buildVendoredReleasesApiUrl(baseUrl, repository), {
709
+ headers: buildVendoredReleaseApiHeaders()
710
+ })
711
+
712
+ if (!response.ok) {
713
+ throw new Error(
714
+ `Failed to query vendored releases for ${repository}: HTTP ${response.status}`
715
+ )
716
+ }
717
+
718
+ const releases = await response.json()
719
+ return Array.isArray(releases) ? releases : []
720
+ }
721
+
722
+ function buildVendoredReleasesApiUrl(baseUrl, repository) {
723
+ const normalizedBaseUrl = String(baseUrl || "https://github.com").replace(/\/+$/u, "")
724
+
725
+ try {
726
+ const parsedUrl = new URL(normalizedBaseUrl)
727
+
728
+ if (parsedUrl.hostname === "github.com") {
729
+ return `https://api.github.com/repos/${repository}/releases?per_page=20`
730
+ }
731
+
732
+ if (parsedUrl.hostname === "api.github.com") {
733
+ return `${normalizedBaseUrl}/repos/${repository}/releases?per_page=20`
734
+ }
735
+ } catch {
736
+ // Fall through to the generic repository API path.
737
+ }
738
+
739
+ return `${normalizedBaseUrl}/repos/${repository}/releases?per_page=20`
740
+ }
741
+
742
+ function buildVendoredReleaseApiHeaders() {
743
+ const token =
744
+ process.env.GITHUB_TOKEN?.trim() ||
745
+ process.env.GH_TOKEN?.trim() ||
746
+ process.env.HAGISCRIPT_GITHUB_TOKEN?.trim()
747
+
748
+ return {
749
+ Accept: "application/vnd.github+json",
750
+ "User-Agent": "hagiscript-vendored-runtime",
751
+ ...(token ? { Authorization: `Bearer ${token}` } : {})
752
+ }
753
+ }
754
+
755
+ function isVendoredAssetMissingError(error) {
756
+ return Boolean(
757
+ error instanceof Error &&
758
+ /Failed to download vendored asset .*: HTTP 404$/u.test(error.message.trim())
759
+ )
760
+ }
761
+
559
762
  async function extractVendoredArchive(archivePath, stagingDirectory, archiveKind) {
560
763
  await rm(stagingDirectory, { recursive: true, force: true })
561
764
  await mkdir(stagingDirectory, { recursive: true })