@mpgd/cli 0.29.0 → 0.30.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/dist/hosted-pwa-deployment.js +261 -84
- package/dist/index.js +175 -1
- package/dist/microsoft-store-package-acceptance.js +19 -8
- package/dist/offline-playtest.js +20 -4
- package/package.json +7 -7
|
@@ -375,6 +375,8 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
375
375
|
const references = [...cssReferences];
|
|
376
376
|
const metaRefreshContents = [];
|
|
377
377
|
const embeddedDocuments = [];
|
|
378
|
+
const stylesheetHrefs = new Set();
|
|
379
|
+
const activeStylesheets = new Set();
|
|
378
380
|
// Attributes only exist on start tags; scanning raw text would treat prose
|
|
379
381
|
// like `Set href="..."` as a reference. Extract within element start tags,
|
|
380
382
|
// with object data URLs and srcset candidates handled per tag.
|
|
@@ -385,18 +387,31 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
385
387
|
const resourceNames = tagName === 'object'
|
|
386
388
|
? ['href', 'src', 'poster', 'data']
|
|
387
389
|
: ['href', 'src', 'poster', 'xlink:href'];
|
|
388
|
-
// Tokenize real attribute name/value pairs
|
|
389
|
-
//
|
|
390
|
+
// Tokenize real attribute name/value pairs exactly once so every
|
|
391
|
+
// attribute-driven check (resource names, srcset, style, srcdoc, meta)
|
|
392
|
+
// shares one parse. Quoted values of unrelated attributes
|
|
393
|
+
// (title='See srcset="./missing.png"') can never leak references, and
|
|
394
|
+
// duplicate attributes keep their first occurrence like HTML parsing.
|
|
395
|
+
const seenAttributeNames = new Set();
|
|
396
|
+
const attributePairs = [];
|
|
397
|
+
for (const attribute of attributes.matchAll(/([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gu)) {
|
|
398
|
+
const name = (attribute[1] ?? '').toLowerCase();
|
|
399
|
+
if (seenAttributeNames.has(name)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
seenAttributeNames.add(name);
|
|
403
|
+
attributePairs.push({ name, value: attribute[2] ?? attribute[3] ?? attribute[4] ?? '' });
|
|
404
|
+
}
|
|
405
|
+
// An iframe srcdoc overrides src even when its value is empty.
|
|
406
|
+
const attributesHasSrcdoc = tagName === 'iframe'
|
|
407
|
+
&& attributePairs.some(({ name }) => name === 'srcdoc');
|
|
390
408
|
let isMetaRefresh = false;
|
|
391
409
|
let metaContent;
|
|
392
410
|
let styleValue;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
for (const attribute of attributes.matchAll(/([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gu)) {
|
|
398
|
-
const name = (attribute[1] ?? '').toLowerCase();
|
|
399
|
-
const value = attribute[2] ?? attribute[3] ?? attribute[4] ?? '';
|
|
411
|
+
let srcsetValue;
|
|
412
|
+
let linkRel;
|
|
413
|
+
let linkHref;
|
|
414
|
+
for (const { name, value } of attributePairs) {
|
|
400
415
|
if (tagName === 'meta' && name === 'http-equiv' && value.trim().toLowerCase() === 'refresh') {
|
|
401
416
|
isMetaRefresh = true;
|
|
402
417
|
}
|
|
@@ -406,7 +421,16 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
406
421
|
if (name === 'style') {
|
|
407
422
|
styleValue = value;
|
|
408
423
|
}
|
|
409
|
-
if (
|
|
424
|
+
if (name === 'srcset') {
|
|
425
|
+
srcsetValue = value;
|
|
426
|
+
}
|
|
427
|
+
if (tagName === 'link' && name === 'rel') {
|
|
428
|
+
linkRel = value.trim().toLowerCase();
|
|
429
|
+
}
|
|
430
|
+
if (tagName === 'link' && name === 'href') {
|
|
431
|
+
linkHref = value;
|
|
432
|
+
}
|
|
433
|
+
if (!resourceNames.includes(name) && name !== 'srcdoc') {
|
|
410
434
|
continue;
|
|
411
435
|
}
|
|
412
436
|
if (name === 'srcdoc' && value.length > 0) {
|
|
@@ -415,49 +439,30 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
415
439
|
embeddedDocuments.push(decodeHtmlReferences(value));
|
|
416
440
|
continue;
|
|
417
441
|
}
|
|
418
|
-
if (
|
|
442
|
+
if (value.length > 0) {
|
|
419
443
|
if (tagName === 'iframe' && name === 'src' && attributesHasSrcdoc) {
|
|
420
444
|
continue;
|
|
421
445
|
}
|
|
422
446
|
references.push(value);
|
|
423
447
|
}
|
|
424
448
|
}
|
|
449
|
+
// Remember stylesheet links by their raw href so only files the browser
|
|
450
|
+
// actually applies as CSS join the stylesheet scan; a plain anchor to a
|
|
451
|
+
// .css file is just a downloadable reference. rel/href order-agnostic.
|
|
452
|
+
if (tagName === 'link' && linkHref !== undefined && isStylesheetRel(linkRel)) {
|
|
453
|
+
stylesheetHrefs.add(linkHref);
|
|
454
|
+
}
|
|
425
455
|
if (isMetaRefresh && metaContent !== undefined) {
|
|
426
456
|
metaRefreshContents.push(metaContent);
|
|
427
457
|
}
|
|
428
458
|
if (styleValue !== undefined) {
|
|
429
459
|
collectInlineCssReferences(decodeHtmlReferences(styleValue), references);
|
|
430
460
|
}
|
|
431
|
-
if (
|
|
461
|
+
if (srcsetValue === undefined) {
|
|
432
462
|
continue;
|
|
433
463
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const candidates = decodeHtmlReferences(rawCandidates);
|
|
437
|
-
// WHATWG srcset tokenizing: whitespace-split tokens alternate between a
|
|
438
|
-
// candidate URL and its descriptors; a numeric single-token value like
|
|
439
|
-
// srcset="404" stays a URL, while densities after a URL are skipped.
|
|
440
|
-
let holdingUrl = false;
|
|
441
|
-
for (const token of candidates.split(/\s+/u)) {
|
|
442
|
-
for (const piece of token.split(/(?<=\d[xw]),/u)) {
|
|
443
|
-
const trimmed = piece.replace(/^[,]+|[,]+$/gu, '');
|
|
444
|
-
if (trimmed.length === 0) {
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
const isDescriptor = /^[\d.]+[wx]?$/u.test(trimmed);
|
|
448
|
-
if (holdingUrl && isDescriptor) {
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
references.push(trimmed);
|
|
452
|
-
// A trailing comma on a URL token ends the candidate, so the next
|
|
453
|
-
// token starts fresh rather than being read as a descriptor.
|
|
454
|
-
const endedCandidate = /,$/u.test(piece);
|
|
455
|
-
holdingUrl = !isDescriptor && !endedCandidate;
|
|
456
|
-
}
|
|
457
|
-
if (/^[\d.]+[wx],?$/u.test(token)) {
|
|
458
|
-
holdingUrl = false;
|
|
459
|
-
}
|
|
460
|
-
}
|
|
464
|
+
// Shared WHATWG srcset tokenizer (also used by the legal-page pass).
|
|
465
|
+
pushSrcsetCandidates(decodeHtmlReferences(srcsetValue), references);
|
|
461
466
|
}
|
|
462
467
|
// Embedded srcdoc documents resolve their resources against this
|
|
463
468
|
// deployment; scan them with the full tokenizer (resource attributes,
|
|
@@ -474,48 +479,6 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
474
479
|
references.push(target[1] ?? target[2] ?? target[3] ?? '');
|
|
475
480
|
}
|
|
476
481
|
}
|
|
477
|
-
// Referenced local stylesheets carry their own url()/@import targets;
|
|
478
|
-
// resolve them relative to each stylesheet's directory.
|
|
479
|
-
const stylesheetReferences = [];
|
|
480
|
-
for (const reference of [...referenced]) {
|
|
481
|
-
if (!reference.endsWith('.css') || !deploymentPaths.has(reference)) {
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
const cssContent = readFileSync(join(deploymentRoot, reference), 'utf8');
|
|
485
|
-
const cssDir = reference.includes('/')
|
|
486
|
-
? `./${reference.slice(0, reference.lastIndexOf('/'))}`
|
|
487
|
-
: '.';
|
|
488
|
-
for (const cssUrl of cssContent.matchAll(/url\(\s*(['"]?)([^'")]+)\1\s*\)/giu)) {
|
|
489
|
-
const raw = (cssUrl[2] ?? '').trim();
|
|
490
|
-
if (raw.length === 0 || raw.startsWith('data:') || raw.startsWith('#')) {
|
|
491
|
-
continue;
|
|
492
|
-
}
|
|
493
|
-
const scheme = /^([a-z][a-z0-9+.-]*):/iu.exec(raw);
|
|
494
|
-
if (scheme !== null && scheme[1] !== undefined && scheme[1].toLowerCase() !== 'https' && scheme[1].toLowerCase() !== 'http') {
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
const schemeText = scheme?.[1] ?? '';
|
|
498
|
-
let cssPath = scheme !== null ? raw.slice(schemeText.length + 1) : raw;
|
|
499
|
-
if (cssPath.startsWith('//')) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
cssPath = cssPath.split('?')[0]?.split('#')[0] ?? cssPath;
|
|
503
|
-
const cssTarget = cssPath.startsWith('/') ? cssPath.slice(1) : `${cssDir}/${cssPath}`;
|
|
504
|
-
const resolved = posix.normalize(cssTarget);
|
|
505
|
-
if (!resolved.startsWith('../')) {
|
|
506
|
-
stylesheetReferences.push(resolved);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
for (const importMatch of cssContent.matchAll(/@import\s+(['"])([^'"]+)\1/giu)) {
|
|
510
|
-
const raw = (importMatch[2] ?? '').trim();
|
|
511
|
-
if (raw.length > 0 && !raw.startsWith('/') && !raw.startsWith('data:')) {
|
|
512
|
-
stylesheetReferences.push(posix.normalize(`${cssDir}/${raw.split('?')[0] ?? raw}`));
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
for (const stylesheetReference of stylesheetReferences) {
|
|
517
|
-
referenced.add(stylesheetReference);
|
|
518
|
-
}
|
|
519
482
|
for (const rawReference of references) {
|
|
520
483
|
const reference = decodeHtmlReferences(rawReference).trim();
|
|
521
484
|
const schemeSeparated = /^([a-z][a-z0-9+.-]*):/iu.exec(reference);
|
|
@@ -542,9 +505,6 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
542
505
|
continue;
|
|
543
506
|
}
|
|
544
507
|
}
|
|
545
|
-
if (process.env.MPGD_DEBUG_SRCDOC === '1') {
|
|
546
|
-
console.error('DEBUG-REF', JSON.stringify(reference.slice(0, 60)), '->', JSON.stringify(effectiveReference.slice(0, 60)));
|
|
547
|
-
}
|
|
548
508
|
const withoutQuery = effectiveReference.split(/[?#]/u)[0] ?? effectiveReference;
|
|
549
509
|
if (withoutQuery.length === 0) {
|
|
550
510
|
continue;
|
|
@@ -576,8 +536,84 @@ function verifyIndexReferences(deploymentRoot, deploymentFiles) {
|
|
|
576
536
|
if (normalized === '.' || normalized.endsWith('/')) {
|
|
577
537
|
normalized = `${normalized === '.' ? '' : normalized}index.html`;
|
|
578
538
|
}
|
|
539
|
+
if (stylesheetHrefs.has(rawReference)) {
|
|
540
|
+
activeStylesheets.add(normalized);
|
|
541
|
+
}
|
|
579
542
|
referenced.add(normalized);
|
|
580
543
|
}
|
|
544
|
+
// Stylesheet links (and the @import chain they start) carry their own
|
|
545
|
+
// url()/@import targets, resolved relative to each stylesheet's directory.
|
|
546
|
+
// Comments are stripped first, external URLs are skipped, root-relative
|
|
547
|
+
// targets resolve from the deployment root, percent-encoded segments
|
|
548
|
+
// decode, escaping targets are rejected, and chained imports follow with a
|
|
549
|
+
// visited set so circular imports cannot loop forever.
|
|
550
|
+
const visitedStylesheets = new Set();
|
|
551
|
+
const stylesheetQueue = [...activeStylesheets];
|
|
552
|
+
while (stylesheetQueue.length > 0) {
|
|
553
|
+
const stylesheet = stylesheetQueue.shift();
|
|
554
|
+
if (stylesheet === undefined || visitedStylesheets.has(stylesheet)) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
visitedStylesheets.add(stylesheet);
|
|
558
|
+
const { cssUrls, cssImports } = extractStylesheetTargets(readFileSync(join(deploymentRoot, stylesheet), 'utf8'));
|
|
559
|
+
const cssDir = stylesheet.includes('/')
|
|
560
|
+
? `./${stylesheet.slice(0, stylesheet.lastIndexOf('/'))}`
|
|
561
|
+
: '.';
|
|
562
|
+
const resolvedUrls = [];
|
|
563
|
+
const resolvedImports = [];
|
|
564
|
+
const collectCssTarget = (rawTarget, sink) => {
|
|
565
|
+
const raw = decodeCssEscapes(rawTarget.trim());
|
|
566
|
+
if (raw.length === 0 || raw.startsWith('data:') || raw.startsWith('#')) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/iu.exec(raw);
|
|
570
|
+
if (scheme !== null
|
|
571
|
+
&& scheme[1] !== undefined
|
|
572
|
+
&& scheme[1].toLowerCase() !== 'https'
|
|
573
|
+
&& scheme[1].toLowerCase() !== 'http') {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const schemeText = scheme?.[1] ?? '';
|
|
577
|
+
let cssPath = scheme !== null ? raw.slice(schemeText.length + 1) : raw;
|
|
578
|
+
// Cross-scheme authority URLs (https://host/x, http:/host/x) are
|
|
579
|
+
// external; only same-scheme relative forms like https:./file.png
|
|
580
|
+
// resolve against the deployment.
|
|
581
|
+
if (scheme !== null && cssPath.startsWith('/')) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// Scheme-relative URLs without a scheme token (//host/x) are external.
|
|
585
|
+
if (scheme === null && cssPath.startsWith('//')) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
cssPath = cssPath.split('?')[0]?.split('#')[0] ?? cssPath;
|
|
589
|
+
const cssTarget = cssPath.startsWith('/') ? cssPath.slice(1) : `${cssDir}/${cssPath}`;
|
|
590
|
+
const resolved = posix.normalize(cssTarget);
|
|
591
|
+
if (resolved.startsWith('../') || resolved === '..' || posix.isAbsolute(resolved)) {
|
|
592
|
+
throw new Error(`The stylesheet ${stylesheet} references a path outside the artifact root: ${raw}`);
|
|
593
|
+
}
|
|
594
|
+
sink.push(decodeReferenceSegments(resolved, `stylesheet ${stylesheet}`));
|
|
595
|
+
};
|
|
596
|
+
for (const cssUrl of cssUrls) {
|
|
597
|
+
collectCssTarget(cssUrl, resolvedUrls);
|
|
598
|
+
}
|
|
599
|
+
for (const cssImport of cssImports) {
|
|
600
|
+
collectCssTarget(cssImport, resolvedImports);
|
|
601
|
+
}
|
|
602
|
+
for (const resolvedUrl of resolvedUrls) {
|
|
603
|
+
referenced.add(resolvedUrl);
|
|
604
|
+
}
|
|
605
|
+
for (const resolvedImport of resolvedImports) {
|
|
606
|
+
referenced.add(resolvedImport);
|
|
607
|
+
// Imports are stylesheets by syntax whatever their URL looks like, and
|
|
608
|
+
// deduplication against earlier references must not suppress
|
|
609
|
+
// traversal: the browser still applies the imported file as CSS.
|
|
610
|
+
if (deploymentPaths.has(resolvedImport)
|
|
611
|
+
&& !visitedStylesheets.has(resolvedImport)
|
|
612
|
+
&& !stylesheetQueue.includes(resolvedImport)) {
|
|
613
|
+
stylesheetQueue.push(resolvedImport);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
581
617
|
for (const reference of [...referenced].sort()) {
|
|
582
618
|
if (pagesControlArtifactNames.has(reference)) {
|
|
583
619
|
throw new Error(`The deployment index.html references the Pages control file ${reference}; `
|
|
@@ -806,6 +842,147 @@ function collectEmbeddedDocumentReferences(document, references) {
|
|
|
806
842
|
}
|
|
807
843
|
}
|
|
808
844
|
/** Tokenize srcset candidates into the reference list. */
|
|
845
|
+
/** Whether a link rel token list applies the target as a stylesheet. */
|
|
846
|
+
function isStylesheetRel(rel) {
|
|
847
|
+
return rel !== undefined && rel.split(/\s+/u).includes('stylesheet');
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Extract live url() targets and @import targets from a stylesheet with a
|
|
851
|
+
* small string-aware tokenizer:
|
|
852
|
+
*
|
|
853
|
+
* - comments are skipped;
|
|
854
|
+
* - url()/@import text inside quoted strings is inert content, so a
|
|
855
|
+
* `content: "url(./missing.png)"` rule never becomes a reference;
|
|
856
|
+
* - @import is honored only before the first top-level style block, because
|
|
857
|
+
* CSS parsing makes later imports inert;
|
|
858
|
+
* - @import supports both quoted-string and url() forms.
|
|
859
|
+
*/
|
|
860
|
+
function extractStylesheetTargets(css) {
|
|
861
|
+
const cssUrls = [];
|
|
862
|
+
const cssImports = [];
|
|
863
|
+
let index = 0;
|
|
864
|
+
let sawTopLevelBlock = false;
|
|
865
|
+
const skipString = () => {
|
|
866
|
+
const quote = css[index] ?? '';
|
|
867
|
+
index += 1;
|
|
868
|
+
while (index < css.length) {
|
|
869
|
+
const inner = css[index] ?? '';
|
|
870
|
+
index += 1;
|
|
871
|
+
if (inner === '\\') {
|
|
872
|
+
index += 1;
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
if (inner === quote) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
while (index < css.length) {
|
|
881
|
+
const char = css[index] ?? '';
|
|
882
|
+
if (char === '"' || char === "'") {
|
|
883
|
+
skipString();
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (char === '/' && (css[index + 1] ?? '') === '*') {
|
|
887
|
+
const end = css.indexOf('*/', index + 2);
|
|
888
|
+
index = end === -1 ? css.length : end + 2;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (char === '{') {
|
|
892
|
+
sawTopLevelBlock = true;
|
|
893
|
+
index += 1;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
const rest = css.slice(index);
|
|
897
|
+
const urlMatch = /^url\(/iu.exec(rest);
|
|
898
|
+
if (urlMatch !== null) {
|
|
899
|
+
index += urlMatch[0].length;
|
|
900
|
+
let quote = '';
|
|
901
|
+
let argument = '';
|
|
902
|
+
while (index < css.length) {
|
|
903
|
+
const inner = css[index] ?? '';
|
|
904
|
+
if (quote === '' && (inner === '"' || inner === "'")) {
|
|
905
|
+
quote = inner;
|
|
906
|
+
index += 1;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
if (quote !== '' && inner === quote) {
|
|
910
|
+
quote = '';
|
|
911
|
+
index += 1;
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (quote !== '' && inner === '\\') {
|
|
915
|
+
argument += inner + (css[index + 1] ?? '');
|
|
916
|
+
index += 2;
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
if (quote === '' && inner === ')') {
|
|
920
|
+
index += 1;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
argument += inner;
|
|
924
|
+
index += 1;
|
|
925
|
+
}
|
|
926
|
+
cssUrls.push(argument.trim());
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const importMatch = !sawTopLevelBlock ? /^@import\b/iu.exec(rest) : null;
|
|
930
|
+
if (importMatch !== null) {
|
|
931
|
+
index += importMatch[0].length;
|
|
932
|
+
let captured = '';
|
|
933
|
+
while (index < css.length) {
|
|
934
|
+
const inner = css[index] ?? '';
|
|
935
|
+
if (inner === ';') {
|
|
936
|
+
index += 1;
|
|
937
|
+
break;
|
|
938
|
+
}
|
|
939
|
+
captured += inner;
|
|
940
|
+
index += 1;
|
|
941
|
+
}
|
|
942
|
+
const trimmed = captured.trim();
|
|
943
|
+
const quotedForm = /^(['"])([\s\S]*)\1$/u.exec(trimmed);
|
|
944
|
+
if (quotedForm !== null) {
|
|
945
|
+
cssImports.push((quotedForm[2] ?? '').trim());
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
const urlForm = /^url\(\s*(['"]?)([^'")]*)\1\s*\)$/iu.exec(trimmed);
|
|
949
|
+
if (urlForm !== null) {
|
|
950
|
+
cssImports.push((urlForm[2] ?? '').trim());
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
index += 1;
|
|
956
|
+
}
|
|
957
|
+
return { cssUrls, cssImports };
|
|
958
|
+
}
|
|
959
|
+
/** Decode CSS string escapes (backslash-character and hex forms) like browsers do. */
|
|
960
|
+
function decodeCssEscapes(value) {
|
|
961
|
+
if (!value.includes('\\')) {
|
|
962
|
+
return value;
|
|
963
|
+
}
|
|
964
|
+
return value.replace(/\\(?:([0-9a-fA-F]{1,6})\s?|(.))/gu, (_match, hex, plain) => {
|
|
965
|
+
if (hex === undefined) {
|
|
966
|
+
return plain ?? '';
|
|
967
|
+
}
|
|
968
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
969
|
+
return codePoint > 1114111 || Number.isNaN(codePoint)
|
|
970
|
+
? '\uFFFD'
|
|
971
|
+
: String.fromCodePoint(codePoint);
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
/** Percent-decode each path segment, mirroring the HTML reference handling. */
|
|
975
|
+
function decodeReferenceSegments(path, label) {
|
|
976
|
+
try {
|
|
977
|
+
return path
|
|
978
|
+
.split('/')
|
|
979
|
+
.map((segment) => decodeURIComponent(segment))
|
|
980
|
+
.join('/');
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
throw new Error(`${label} references a malformed percent-encoded URL: ${path}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
809
986
|
function pushSrcsetCandidates(candidates, references) {
|
|
810
987
|
let holdingUrl = false;
|
|
811
988
|
for (const token of candidates.split(/\s+/u)) {
|
package/dist/index.js
CHANGED
|
@@ -143,6 +143,8 @@ const entryCommand = defineI18n({
|
|
|
143
143
|
ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
|
|
144
144
|
}),
|
|
145
145
|
run: () => {
|
|
146
|
+
console.info('Use a sub-command: game, target, kit.');
|
|
147
|
+
console.info('Run "pnpm mpgd --help" for available commands.');
|
|
146
148
|
},
|
|
147
149
|
});
|
|
148
150
|
const gameCommand = defineI18n({
|
|
@@ -478,6 +480,9 @@ const gameCommand = defineI18n({
|
|
|
478
480
|
outputDir,
|
|
479
481
|
maximumBytes,
|
|
480
482
|
});
|
|
483
|
+
console.info(`Offline playtest (test only): ${result.entryFile}`);
|
|
484
|
+
console.info(`Evidence: ${result.evidenceFile}`);
|
|
485
|
+
console.info('Not a release target, deployment artifact, PWA, or store-submission package.');
|
|
481
486
|
},
|
|
482
487
|
}),
|
|
483
488
|
icons: defineI18n({
|
|
@@ -534,10 +539,12 @@ const gameCommand = defineI18n({
|
|
|
534
539
|
}),
|
|
535
540
|
])),
|
|
536
541
|
run: () => {
|
|
542
|
+
console.info('Use "mpgd game icons generate <game>", verify, or inspect.');
|
|
537
543
|
},
|
|
538
544
|
}),
|
|
539
545
|
},
|
|
540
546
|
run: () => {
|
|
547
|
+
console.info('Use "mpgd game create <directory>", "mpgd game accept <game>", "mpgd game offline-playtest <game>", or "mpgd game icons generate <game>".');
|
|
541
548
|
},
|
|
542
549
|
});
|
|
543
550
|
const defaultLegalDir = 'legal';
|
|
@@ -699,9 +706,12 @@ function acceptGame(input) {
|
|
|
699
706
|
MPGD_GAMEPLAY_E2E_REPORT_FILE: gameplayE2EReportFile,
|
|
700
707
|
},
|
|
701
708
|
});
|
|
709
|
+
console.info(`[mpgd:accept] JSON report: ${result.jsonFile}`);
|
|
710
|
+
console.info(`[mpgd:accept] Markdown report: ${result.markdownFile}`);
|
|
702
711
|
if (result.report.status === 'failed') {
|
|
703
712
|
throw new Error(`Game acceptance failed. Report: ${result.markdownFile}`);
|
|
704
713
|
}
|
|
714
|
+
console.info('[mpgd:accept] passed');
|
|
705
715
|
}
|
|
706
716
|
function assertGameAcceptanceRoot(gameRoot) {
|
|
707
717
|
const resolved = path.resolve(gameRoot);
|
|
@@ -785,6 +795,7 @@ const legalCommand = defineI18n({
|
|
|
785
795
|
...resolveLegalCommandOptions(ctx.values),
|
|
786
796
|
check: false,
|
|
787
797
|
});
|
|
798
|
+
console.info(`Built legal site: ${result.outDir}`);
|
|
788
799
|
},
|
|
789
800
|
}),
|
|
790
801
|
check: defineI18n({
|
|
@@ -800,10 +811,12 @@ const legalCommand = defineI18n({
|
|
|
800
811
|
...resolveLegalCommandOptions(ctx.values),
|
|
801
812
|
check: true,
|
|
802
813
|
});
|
|
814
|
+
console.info(`Legal site checked: ${result.outDir}`);
|
|
803
815
|
},
|
|
804
816
|
}),
|
|
805
817
|
},
|
|
806
818
|
run: () => {
|
|
819
|
+
console.info('Use "mpgd legal build" or "mpgd legal check".');
|
|
807
820
|
},
|
|
808
821
|
});
|
|
809
822
|
const targetCommand = defineI18n({
|
|
@@ -873,9 +886,10 @@ const targetCommand = defineI18n({
|
|
|
873
886
|
const resolvedKitPath = configuredKitPath === undefined
|
|
874
887
|
? detectedKitRoot
|
|
875
888
|
: assertKitRoot(path.resolve(configuredKitPath));
|
|
889
|
+
const canonicalGameRoot = existsSync(gameRoot) ? realpathSync(gameRoot) : gameRoot;
|
|
876
890
|
const defaultKitPath = resolvedKitPath === undefined
|
|
877
891
|
? defaultMpgdKitPath
|
|
878
|
-
: toTemplatePath(path.relative(
|
|
892
|
+
: toTemplatePath(path.relative(canonicalGameRoot, resolvedKitPath) || '.');
|
|
879
893
|
const dryRun = ctx.values['dry-run'] === true;
|
|
880
894
|
const adapterDependencyVersion = resolveMicrosoftStoreAdapterDependencyVersion(requireMpgdDependencyVersion(resolveMpgdDependencyVersionReplacements({
|
|
881
895
|
workspace: false,
|
|
@@ -888,7 +902,9 @@ const targetCommand = defineI18n({
|
|
|
888
902
|
adapterDependencyVersion,
|
|
889
903
|
dryRun,
|
|
890
904
|
});
|
|
905
|
+
console.info(`${dryRun ? 'Would update' : 'Updated'} Microsoft Store starter workflow: ${result.changedFiles.length} file(s).`);
|
|
891
906
|
for (const file of result.changedFiles) {
|
|
907
|
+
console.info(`- ${file}`);
|
|
892
908
|
}
|
|
893
909
|
},
|
|
894
910
|
}),
|
|
@@ -1095,6 +1111,136 @@ const targetCommand = defineI18n({
|
|
|
1095
1111
|
report: verification,
|
|
1096
1112
|
markdown: renderHostedPwaVerificationMarkdown(verification),
|
|
1097
1113
|
});
|
|
1114
|
+
console.info(`Hosted PWA deployment verified: ${verification.verifiedGameFileCount} game `
|
|
1115
|
+
+ `file(s) from revision ${verification.revision} under the `
|
|
1116
|
+
+ `${verification.host}/${verification.profile} profile `
|
|
1117
|
+
+ `(routes: ${verification.workerRoutes.include.join(', ')}).`);
|
|
1118
|
+
},
|
|
1119
|
+
}),
|
|
1120
|
+
'preview-versions': defineI18n({
|
|
1121
|
+
name: 'preview-versions',
|
|
1122
|
+
description: 'Preview candidate platform version numbers for a release ledger without reserving them.',
|
|
1123
|
+
resource: commandResource({
|
|
1124
|
+
en: 'Preview candidate platform version numbers for a release ledger without reserving them.',
|
|
1125
|
+
ko: '릴리스 원장에 대한 플랫폼 버전 번호 후보를 예약 없이 미리 확인합니다.',
|
|
1126
|
+
}, {
|
|
1127
|
+
'ledger-file': {
|
|
1128
|
+
en: 'Path to the platform version ledger JSON file (read-only).',
|
|
1129
|
+
ko: '플랫폼 버전 원장 JSON 파일 경로(읽기 전용).',
|
|
1130
|
+
},
|
|
1131
|
+
'plan-file': {
|
|
1132
|
+
en: 'Optional prepared release plan JSON to revalidate and extend.',
|
|
1133
|
+
ko: '재검증하고 확장할 선택적 준비된 릴리스 계획 JSON.',
|
|
1134
|
+
},
|
|
1135
|
+
'game-id': {
|
|
1136
|
+
en: 'Game identifier used in the release buildId.',
|
|
1137
|
+
ko: '릴리스 buildId에 사용할 게임 식별자.',
|
|
1138
|
+
},
|
|
1139
|
+
'game-version': {
|
|
1140
|
+
en: 'Final game SemVer for the release.',
|
|
1141
|
+
ko: '릴리스의 최종 게임 SemVer.',
|
|
1142
|
+
},
|
|
1143
|
+
'source-git-sha': {
|
|
1144
|
+
en: 'Full 40-character Git SHA of the game source.',
|
|
1145
|
+
ko: '게임 소스의 전체 40자 Git SHA.',
|
|
1146
|
+
},
|
|
1147
|
+
'kit-git-sha': {
|
|
1148
|
+
en: 'Full 40-character Git SHA of the mpgd-kit checkout.',
|
|
1149
|
+
ko: 'mpgd-kit 체크아웃의 전체 40자 Git SHA.',
|
|
1150
|
+
},
|
|
1151
|
+
'target-config-digest': {
|
|
1152
|
+
en: '64-character SHA-256 digest of the game-owned target configuration.',
|
|
1153
|
+
ko: '게임 소유 타깃 설정의 64자 SHA-256 다이제스트.',
|
|
1154
|
+
},
|
|
1155
|
+
targets: {
|
|
1156
|
+
en: 'Comma-separated targets, optionally "name:hosted-content-only" for microsoft-store.',
|
|
1157
|
+
ko: '쉼표로 구분된 타깃. microsoft-store는 "name:hosted-content-only" 지정 가능.',
|
|
1158
|
+
},
|
|
1159
|
+
json: {
|
|
1160
|
+
en: 'Emit the candidate allocation as JSON.',
|
|
1161
|
+
ko: '후보 할당을 JSON으로 출력합니다.',
|
|
1162
|
+
},
|
|
1163
|
+
'kit-path': {
|
|
1164
|
+
en: 'Path to the mpgd-kit checkout.',
|
|
1165
|
+
ko: 'mpgd-kit 체크아웃 경로.',
|
|
1166
|
+
},
|
|
1167
|
+
}),
|
|
1168
|
+
args: {
|
|
1169
|
+
'ledger-file': {
|
|
1170
|
+
type: 'string',
|
|
1171
|
+
required: true,
|
|
1172
|
+
description: 'Path to the platform version ledger JSON file (read-only).',
|
|
1173
|
+
},
|
|
1174
|
+
'plan-file': {
|
|
1175
|
+
type: 'string',
|
|
1176
|
+
required: false,
|
|
1177
|
+
description: 'Optional prepared release plan JSON to revalidate and extend.',
|
|
1178
|
+
},
|
|
1179
|
+
'game-id': {
|
|
1180
|
+
type: 'string',
|
|
1181
|
+
required: true,
|
|
1182
|
+
description: 'Game identifier used in the release buildId.',
|
|
1183
|
+
},
|
|
1184
|
+
'game-version': {
|
|
1185
|
+
type: 'string',
|
|
1186
|
+
required: true,
|
|
1187
|
+
description: 'Final game SemVer for the release.',
|
|
1188
|
+
},
|
|
1189
|
+
'source-git-sha': {
|
|
1190
|
+
type: 'string',
|
|
1191
|
+
required: true,
|
|
1192
|
+
description: 'Full 40-character Git SHA of the game source.',
|
|
1193
|
+
},
|
|
1194
|
+
'kit-git-sha': {
|
|
1195
|
+
type: 'string',
|
|
1196
|
+
required: true,
|
|
1197
|
+
description: 'Full 40-character Git SHA of the mpgd-kit checkout.',
|
|
1198
|
+
},
|
|
1199
|
+
'target-config-digest': {
|
|
1200
|
+
type: 'string',
|
|
1201
|
+
required: true,
|
|
1202
|
+
description: '64-character SHA-256 digest of the game-owned target configuration.',
|
|
1203
|
+
},
|
|
1204
|
+
targets: {
|
|
1205
|
+
type: 'string',
|
|
1206
|
+
required: true,
|
|
1207
|
+
description: 'Comma-separated targets, optionally "name:hosted-content-only" for microsoft-store.',
|
|
1208
|
+
},
|
|
1209
|
+
json: {
|
|
1210
|
+
type: 'boolean',
|
|
1211
|
+
required: false,
|
|
1212
|
+
description: 'Emit the candidate allocation as JSON.',
|
|
1213
|
+
},
|
|
1214
|
+
'kit-path': {
|
|
1215
|
+
type: 'string',
|
|
1216
|
+
required: false,
|
|
1217
|
+
description: 'Path to the mpgd-kit checkout.',
|
|
1218
|
+
},
|
|
1219
|
+
},
|
|
1220
|
+
run: (ctx) => {
|
|
1221
|
+
const kitPath = resolveKitPathForTarget(ctx.values);
|
|
1222
|
+
const planFile = readOptionalString(ctx.values['plan-file']);
|
|
1223
|
+
const env = {
|
|
1224
|
+
...process.env,
|
|
1225
|
+
MPGD_KIT_PATH: kitPath,
|
|
1226
|
+
MPGD_PREVIEW_LEDGER_FILE: path.resolve(readRequiredCliOption(ctx.values['ledger-file'], '--ledger-file')),
|
|
1227
|
+
MPGD_PREVIEW_GAME_ID: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
|
|
1228
|
+
MPGD_PREVIEW_GAME_VERSION: readRequiredCliOption(ctx.values['game-version'], '--game-version'),
|
|
1229
|
+
MPGD_PREVIEW_SOURCE_GIT_SHA: readRequiredCliOption(ctx.values['source-git-sha'], '--source-git-sha'),
|
|
1230
|
+
MPGD_PREVIEW_KIT_GIT_SHA: readRequiredCliOption(ctx.values['kit-git-sha'], '--kit-git-sha'),
|
|
1231
|
+
MPGD_PREVIEW_TARGET_CONFIG_DIGEST: readRequiredCliOption(ctx.values['target-config-digest'], '--target-config-digest'),
|
|
1232
|
+
MPGD_PREVIEW_TARGETS: readRequiredCliOption(ctx.values.targets, '--targets'),
|
|
1233
|
+
// Always define the optional toggles so ambient values from the
|
|
1234
|
+
// caller's environment cannot override absent flags.
|
|
1235
|
+
MPGD_PREVIEW_PLAN_FILE: planFile === undefined ? '' : path.resolve(planFile),
|
|
1236
|
+
MPGD_PREVIEW_JSON: ctx.values.json === true ? '1' : '',
|
|
1237
|
+
};
|
|
1238
|
+
if (ctx.values.json !== true) {
|
|
1239
|
+
console.info('[mpgd] target preview-versions (read-only candidates, not reservations)');
|
|
1240
|
+
}
|
|
1241
|
+
// --silent keeps pnpm's script echo out of stdout so the JSON document
|
|
1242
|
+
// is machine-readable apart from the framework banner.
|
|
1243
|
+
runPnpm(['--silent', 'preview:platform-versions'], env);
|
|
1098
1244
|
},
|
|
1099
1245
|
}),
|
|
1100
1246
|
'generate-package': defineI18n({
|
|
@@ -1216,6 +1362,7 @@ const targetCommand = defineI18n({
|
|
|
1216
1362
|
jsonFile: path.join(outputDir, 'package-generation.json'),
|
|
1217
1363
|
markdownFile: path.join(outputDir, 'package-generation.md'),
|
|
1218
1364
|
});
|
|
1365
|
+
console.info(`Microsoft Store package ZIP downloaded to ${evidence.archive.file}; package inspection remains required.`);
|
|
1219
1366
|
},
|
|
1220
1367
|
}),
|
|
1221
1368
|
preflight: defineI18n({
|
|
@@ -1294,6 +1441,7 @@ const targetCommand = defineI18n({
|
|
|
1294
1441
|
jsonFile: path.join(outputDir, 'submission-preflight.json'),
|
|
1295
1442
|
markdownFile: path.join(outputDir, 'submission-preflight.md'),
|
|
1296
1443
|
});
|
|
1444
|
+
console.info(`Microsoft Store submission preflight passed: ${Object.keys(evidence.listing.locales).length} listing locale(s), ${evidence.warnings.length} warning(s).`);
|
|
1297
1445
|
},
|
|
1298
1446
|
}),
|
|
1299
1447
|
'accept-package': defineI18n({
|
|
@@ -1408,6 +1556,7 @@ const targetCommand = defineI18n({
|
|
|
1408
1556
|
...(appCertExecutable === undefined ? {} : { appCertExecutable }),
|
|
1409
1557
|
...(makeAppxExecutable === undefined ? {} : { makeAppxExecutable }),
|
|
1410
1558
|
}));
|
|
1559
|
+
console.info(`Microsoft Store package acceptance passed: ${evidence.packages.length} package(s).`);
|
|
1411
1560
|
},
|
|
1412
1561
|
}),
|
|
1413
1562
|
'build-all': defineI18n({
|
|
@@ -1508,6 +1657,7 @@ const targetCommand = defineI18n({
|
|
|
1508
1657
|
}),
|
|
1509
1658
|
},
|
|
1510
1659
|
run: () => {
|
|
1660
|
+
console.info('Use "mpgd target init <target>", "mpgd target build <target>", "mpgd target smoke <target>", "mpgd target generate-package <target>", "mpgd target preflight <target>", or "mpgd target accept-package <target> --packages <paths>".');
|
|
1511
1661
|
},
|
|
1512
1662
|
});
|
|
1513
1663
|
const kitCommand = defineI18n({
|
|
@@ -1526,10 +1676,17 @@ const kitCommand = defineI18n({
|
|
|
1526
1676
|
ko: 'CLI, kit, template, target wrapper 상태를 출력합니다.',
|
|
1527
1677
|
}),
|
|
1528
1678
|
run: () => {
|
|
1679
|
+
console.info(`cli package: ${packageRoot}`);
|
|
1680
|
+
console.info(`mpgd-kit: ${detectedKitRoot ?? 'not detected'}`);
|
|
1681
|
+
console.info(`cli template: ${existsSync(gameTemplateDir) ? 'ok' : 'missing'}`);
|
|
1682
|
+
console.info(`mobile wrapper: ${kitFileStatus('apps/mobile-capacitor/package.json')}`);
|
|
1683
|
+
console.info(`ait wrapper: ${kitFileStatus('apps/target-ait/package.json')}`);
|
|
1684
|
+
console.info(`devvit wrapper: ${kitFileStatus('apps/target-devvit/package.json')}`);
|
|
1529
1685
|
},
|
|
1530
1686
|
}),
|
|
1531
1687
|
},
|
|
1532
1688
|
run: () => {
|
|
1689
|
+
console.info('Use "mpgd kit doctor".');
|
|
1533
1690
|
},
|
|
1534
1691
|
});
|
|
1535
1692
|
function commandResource(description, args = {}) {
|
|
@@ -1758,10 +1915,12 @@ function createGameApp(input) {
|
|
|
1758
1915
|
: [{ ...file, content: renderTemplate(prepared, context) }];
|
|
1759
1916
|
});
|
|
1760
1917
|
if (input.dryRun) {
|
|
1918
|
+
console.info(`Would create ${path.relative(process.cwd(), appDir) || appDir}:`);
|
|
1761
1919
|
const plannedFiles = input.microsoftStore
|
|
1762
1920
|
? [...files.map((file) => file.relativePath), ...listMicrosoftStoreInitializerTemplateFiles()]
|
|
1763
1921
|
: files.map((file) => file.relativePath);
|
|
1764
1922
|
for (const relativePath of [...new Set(plannedFiles)].sort()) {
|
|
1923
|
+
console.info(`- ${path.join(path.relative(process.cwd(), appDir), relativePath)}`);
|
|
1765
1924
|
}
|
|
1766
1925
|
return;
|
|
1767
1926
|
}
|
|
@@ -1796,6 +1955,20 @@ function createGameApp(input) {
|
|
|
1796
1955
|
}
|
|
1797
1956
|
throw error;
|
|
1798
1957
|
}
|
|
1958
|
+
console.info(`Created ${appDir}`);
|
|
1959
|
+
console.info('Next steps:');
|
|
1960
|
+
console.info(` cd ${appDir}`);
|
|
1961
|
+
console.info(' pnpm install -w');
|
|
1962
|
+
console.info(' pnpm dev');
|
|
1963
|
+
console.info('Target builds require an mpgd-kit checkout:');
|
|
1964
|
+
console.info([
|
|
1965
|
+
' mpgd target build-all',
|
|
1966
|
+
`--targets-file ${path.join(appDir, 'mpgd.targets.json')}`,
|
|
1967
|
+
`--targets ${context.recommendedMatrixTargets}`,
|
|
1968
|
+
'--profile staging',
|
|
1969
|
+
'--ait-variant wrapper',
|
|
1970
|
+
'--kit-path <path-to-mpgd-kit>',
|
|
1971
|
+
].join(' '));
|
|
1799
1972
|
}
|
|
1800
1973
|
function assertValidGameName(name) {
|
|
1801
1974
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
@@ -2175,6 +2348,7 @@ function runTargetCommand(input) {
|
|
|
2175
2348
|
const args = input.action === 'build'
|
|
2176
2349
|
? ['build:target', target, profile]
|
|
2177
2350
|
: ['smoke:target', target];
|
|
2351
|
+
console.info(`[mpgd] ${input.action} ${target}`);
|
|
2178
2352
|
runPnpm(args, env);
|
|
2179
2353
|
}
|
|
2180
2354
|
function runTargetMatrix(input) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { closeSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { closeSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { escapeMarkdownInline, escapeMarkdownTable, formatError, relativeOrAbsolute, } from './evidence-io.js';
|
|
@@ -85,10 +85,10 @@ export function runMicrosoftStorePackageAcceptance(input, runtime = createMicros
|
|
|
85
85
|
...packageFiles.map((file) => ({ file, label: 'Microsoft Store package' })),
|
|
86
86
|
]);
|
|
87
87
|
for (const reportFile of reportFiles) {
|
|
88
|
-
|
|
88
|
+
removeEvidenceFile(reportFile);
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
removeEvidenceFile(jsonFile);
|
|
91
|
+
removeEvidenceFile(markdownFile);
|
|
92
92
|
const tempRoot = mkdtempSync(path.join(tmpdir(), 'mpgd-microsoft-store-package-'));
|
|
93
93
|
try {
|
|
94
94
|
const packages = packageFiles.map((packageFile, index) => {
|
|
@@ -136,10 +136,10 @@ export function runMicrosoftStorePackageAcceptance(input, runtime = createMicros
|
|
|
136
136
|
writeFileSync(markdownFile, renderMicrosoftStorePackageAcceptanceMarkdown(evidence));
|
|
137
137
|
}
|
|
138
138
|
catch (error) {
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
removeEvidenceFile(jsonFile);
|
|
140
|
+
removeEvidenceFile(markdownFile);
|
|
141
141
|
for (const reportFile of reportFiles) {
|
|
142
|
-
|
|
142
|
+
removeEvidenceFile(reportFile);
|
|
143
143
|
}
|
|
144
144
|
throw error;
|
|
145
145
|
}
|
|
@@ -170,7 +170,7 @@ function runOptionalWindowsAppCertification(input) {
|
|
|
170
170
|
if (reportFile === undefined) {
|
|
171
171
|
throw new Error('Windows App Certification Kit report path was not prepared.');
|
|
172
172
|
}
|
|
173
|
-
|
|
173
|
+
removeEvidenceFile(reportFile);
|
|
174
174
|
input.runtime.runCommand(appCertExecutable, ['reset']);
|
|
175
175
|
input.runtime.runCommand(appCertExecutable, [
|
|
176
176
|
'test',
|
|
@@ -714,6 +714,17 @@ function sameFile(left, right) {
|
|
|
714
714
|
return false;
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
|
+
/** Remove only the file or link, never traverse a replacement directory link. */
|
|
718
|
+
function removeEvidenceFile(file) {
|
|
719
|
+
try {
|
|
720
|
+
unlinkSync(file);
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
if (!isMissingFileError(error)) {
|
|
724
|
+
throw error;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
717
728
|
function isMissingFileError(error) {
|
|
718
729
|
if (typeof error !== 'object' || error === null) {
|
|
719
730
|
return false;
|
package/dist/offline-playtest.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { decodeHTMLAttribute } from 'entities';
|
|
5
|
-
import { build } from 'esbuild';
|
|
5
|
+
import { build, transform } from 'esbuild';
|
|
6
6
|
export const defaultOfflinePlaytestArtifactDir = 'artifacts/web-preview';
|
|
7
7
|
export const defaultOfflinePlaytestOutputDir = 'artifacts/offline-playtest';
|
|
8
8
|
export const defaultOfflinePlaytestMaximumBytes = 25 * 1024 * 1024;
|
|
@@ -530,7 +530,11 @@ async function bundleEntry(entryFile, context) {
|
|
|
530
530
|
platform: 'browser',
|
|
531
531
|
target: 'es2022',
|
|
532
532
|
keepNames: true,
|
|
533
|
-
|
|
533
|
+
minifyWhitespace: true,
|
|
534
|
+
minifySyntax: true,
|
|
535
|
+
// Random deferred markers must not influence esbuild's short identifier choices.
|
|
536
|
+
// Keep their collision resistance; rename identifiers after resolving the markers.
|
|
537
|
+
minifyIdentifiers: false,
|
|
534
538
|
metafile: true,
|
|
535
539
|
sourcemap: false,
|
|
536
540
|
legalComments: 'none',
|
|
@@ -542,7 +546,18 @@ async function bundleEntry(entryFile, context) {
|
|
|
542
546
|
throw new Error('The offline playtest bundler did not produce JavaScript.');
|
|
543
547
|
}
|
|
544
548
|
recordRetainedBundledAssets(result.metafile, context);
|
|
545
|
-
const
|
|
549
|
+
const resolvedScript = resolveDeferredJavaScriptAssets(scriptOutput.text, deferredAssets, context);
|
|
550
|
+
const { code: script } = await transform(resolvedScript, {
|
|
551
|
+
loader: 'js',
|
|
552
|
+
format: 'esm',
|
|
553
|
+
target: 'es2022',
|
|
554
|
+
keepNames: true,
|
|
555
|
+
minifyIdentifiers: true,
|
|
556
|
+
minifyWhitespace: true,
|
|
557
|
+
minifySyntax: false,
|
|
558
|
+
legalComments: 'none',
|
|
559
|
+
sourcemap: false,
|
|
560
|
+
});
|
|
546
561
|
const stylesheetOutput = result.outputFiles.find((file) => file.path.endsWith('.css'));
|
|
547
562
|
assertNoEntryImportMetaUrl(script);
|
|
548
563
|
assertSupportedBundledRuntime(script);
|
|
@@ -1271,7 +1286,8 @@ function parseStaticElementConstructor(initializer, rawInitializer, allowAnyCrea
|
|
|
1271
1286
|
if (constructor?.[2] === undefined) {
|
|
1272
1287
|
const initializerCodePositions = createCodePositionMap(rawInitializer, true);
|
|
1273
1288
|
const normalizedInitializer = maskNonCode(rawInitializer, initializerCodePositions).trim();
|
|
1274
|
-
|
|
1289
|
+
// The code mask retains the opening quote of a literal so token boundaries survive.
|
|
1290
|
+
const createdElement = /^(?:(globalThis|self|window)\s*\.\s*)?document\s*\.\s*createElement\s*\(\s*["'`]\s*\)\s*$/u.exec(normalizedInitializer);
|
|
1275
1291
|
let openingParenthesis = -1;
|
|
1276
1292
|
for (let index = 0; index < rawInitializer.length; index += 1) {
|
|
1277
1293
|
if (rawInitializer[index] === '(' && initializerCodePositions[index] === 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Gunshi CLI for mpgd-kit starter generation and target workflow orchestration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -52,19 +52,19 @@
|
|
|
52
52
|
"@types/node": "^24.0.0",
|
|
53
53
|
"ttsc": "0.18.4",
|
|
54
54
|
"typescript": "7.0.2",
|
|
55
|
-
"@mpgd/adapter-browser": "0.7.3",
|
|
56
55
|
"@mpgd/adapter-ait": "0.12.1",
|
|
56
|
+
"@mpgd/adapter-devvit": "0.9.7",
|
|
57
|
+
"@mpgd/adapter-browser": "0.7.3",
|
|
58
|
+
"@mpgd/adapter-verse8": "0.3.5",
|
|
57
59
|
"@mpgd/adapter-capacitor": "0.4.10",
|
|
58
|
-
"@mpgd/adapter-devvit": "0.9.6",
|
|
59
|
-
"@mpgd/adapter-verse8": "0.3.4",
|
|
60
60
|
"@mpgd/analytics": "0.3.12",
|
|
61
61
|
"@mpgd/bridge": "0.9.0",
|
|
62
62
|
"@mpgd/catalog": "0.7.2",
|
|
63
|
+
"@mpgd/game-services": "0.15.0",
|
|
63
64
|
"@mpgd/i18n": "0.6.1",
|
|
64
|
-
"@mpgd/game-services": "0.14.0",
|
|
65
65
|
"@mpgd/phaser-assets": "0.4.1",
|
|
66
|
-
"@mpgd/
|
|
67
|
-
"@mpgd/
|
|
66
|
+
"@mpgd/target-config": "0.15.0",
|
|
67
|
+
"@mpgd/platform": "0.12.0"
|
|
68
68
|
},
|
|
69
69
|
"main": "./dist/index.js",
|
|
70
70
|
"types": "./dist/index.d.ts",
|