@playcraft/build 0.0.45 → 0.0.47

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.
@@ -0,0 +1,96 @@
1
+ import { getChannelRuleProfile } from './profiles.js';
2
+ import { validateHtmlArtifact } from './html-validator.js';
3
+ import { validateDeliveryPackage } from './package-validator.js';
4
+ import { scanSourceForChannels, scanSourceForCompliance } from './source-scanner.js';
5
+ export const VALIDATION_REPORT_VERSION = '0.1.0';
6
+ export async function runPlayableValidation(context, stages) {
7
+ const profile = getChannelRuleProfile(context.channel);
8
+ const defaultStages = [...profile.defaultStages];
9
+ if (context.packagePath && profile.optionalStages.includes('delivery-package')) {
10
+ defaultStages.push('delivery-package');
11
+ }
12
+ const enabledStages = new Set(stages ?? defaultStages);
13
+ const results = [];
14
+ if (enabledStages.has('prebuild-source')) {
15
+ results.push(...await scanSourceForCompliance(context));
16
+ }
17
+ if (enabledStages.has('html-artifact')) {
18
+ results.push(...await validateHtmlArtifact(context, profile));
19
+ }
20
+ if (enabledStages.has('delivery-package')) {
21
+ results.push(...await validateDeliveryPackage(context, profile));
22
+ }
23
+ const manualChecklist = buildManualChecklist(profile.rules, enabledStages);
24
+ const externalChecklist = buildExternalChecklist(profile.rules, enabledStages);
25
+ return {
26
+ version: VALIDATION_REPORT_VERSION,
27
+ channel: context.channel,
28
+ buildChain: context.buildChain,
29
+ summary: summarize(results, manualChecklist, externalChecklist),
30
+ results,
31
+ manualChecklist,
32
+ externalChecklist,
33
+ };
34
+ }
35
+ export async function runPlayableSourceValidationForChannels(context, channels) {
36
+ const uniqueChannels = Array.from(new Set(channels));
37
+ const results = await scanSourceForChannels(context, uniqueChannels);
38
+ return {
39
+ version: VALIDATION_REPORT_VERSION,
40
+ channel: uniqueChannels[0] ?? 'playcraft',
41
+ buildChain: context.buildChain,
42
+ summary: summarize(results, [], []),
43
+ results,
44
+ manualChecklist: [],
45
+ externalChecklist: [],
46
+ };
47
+ }
48
+ function buildManualChecklist(rules, enabledStages) {
49
+ return rules
50
+ .filter((rule) => rule.owner === 'manual')
51
+ .filter((rule) => enabledStages.has(rule.stage) || rule.stage === 'runtime-manual')
52
+ .map((rule) => ({
53
+ ruleId: rule.id,
54
+ channel: rule.channel,
55
+ stage: rule.stage,
56
+ message: rule.title,
57
+ recommendation: rule.recommendation,
58
+ owner: rule.owner,
59
+ }));
60
+ }
61
+ function buildExternalChecklist(rules, enabledStages) {
62
+ return rules
63
+ .filter((rule) => rule.owner === 'external')
64
+ .filter((rule) => enabledStages.has(rule.stage) || rule.stage === 'external-validator')
65
+ .map((rule) => ({
66
+ ruleId: rule.id,
67
+ channel: rule.channel,
68
+ stage: rule.stage,
69
+ message: rule.title,
70
+ recommendation: rule.recommendation,
71
+ owner: rule.owner,
72
+ }));
73
+ }
74
+ function summarize(results, manualChecklist, externalChecklist) {
75
+ const errorCount = results.filter((result) => result.status === 'fail').length;
76
+ const warningCount = results.filter((result) => result.status === 'warning').length;
77
+ const manualCount = manualChecklist.length;
78
+ const externalCount = externalChecklist.length;
79
+ let status = 'pass';
80
+ if (errorCount > 0) {
81
+ status = 'failed';
82
+ }
83
+ else if (warningCount > 0) {
84
+ status = 'warning';
85
+ }
86
+ else if (manualCount > 0 || externalCount > 0) {
87
+ status = 'manual-required';
88
+ }
89
+ return {
90
+ status,
91
+ errorCount,
92
+ warningCount,
93
+ manualCount,
94
+ externalCount,
95
+ };
96
+ }
@@ -0,0 +1,17 @@
1
+ export interface ZipEntryInfo {
2
+ path: string;
3
+ compressedSize: number;
4
+ uncompressedSize: number;
5
+ compressionMethod: number;
6
+ isDirectory: boolean;
7
+ }
8
+ export declare class SimpleZipReader {
9
+ private readonly buffer;
10
+ private entriesCache?;
11
+ constructor(buffer: Buffer);
12
+ listEntries(): ZipEntryInfo[];
13
+ readEntryText(entryPath: string): string | null;
14
+ private readCentralDirectory;
15
+ private readEntryData;
16
+ private findEndOfCentralDirectory;
17
+ }
@@ -0,0 +1,84 @@
1
+ import { inflateRawSync } from 'zlib';
2
+ export class SimpleZipReader {
3
+ constructor(buffer) {
4
+ this.buffer = buffer;
5
+ }
6
+ listEntries() {
7
+ return this.readCentralDirectory().map(({ localHeaderOffset: _offset, ...entry }) => entry);
8
+ }
9
+ readEntryText(entryPath) {
10
+ const entry = this.readCentralDirectory().find((item) => item.path === entryPath);
11
+ if (!entry || entry.isDirectory)
12
+ return null;
13
+ const data = this.readEntryData(entry);
14
+ return data.toString('utf-8');
15
+ }
16
+ readCentralDirectory() {
17
+ if (this.entriesCache)
18
+ return this.entriesCache;
19
+ const eocdOffset = this.findEndOfCentralDirectory();
20
+ if (eocdOffset < 0) {
21
+ throw new Error('Invalid ZIP: end of central directory not found');
22
+ }
23
+ const totalEntries = this.buffer.readUInt16LE(eocdOffset + 10);
24
+ const centralDirectoryOffset = this.buffer.readUInt32LE(eocdOffset + 16);
25
+ const entries = [];
26
+ let offset = centralDirectoryOffset;
27
+ for (let i = 0; i < totalEntries; i++) {
28
+ const signature = this.buffer.readUInt32LE(offset);
29
+ if (signature !== 0x02014b50) {
30
+ throw new Error(`Invalid ZIP: bad central directory signature at ${offset}`);
31
+ }
32
+ const compressionMethod = this.buffer.readUInt16LE(offset + 10);
33
+ const compressedSize = this.buffer.readUInt32LE(offset + 20);
34
+ const uncompressedSize = this.buffer.readUInt32LE(offset + 24);
35
+ const fileNameLength = this.buffer.readUInt16LE(offset + 28);
36
+ const extraLength = this.buffer.readUInt16LE(offset + 30);
37
+ const commentLength = this.buffer.readUInt16LE(offset + 32);
38
+ const localHeaderOffset = this.buffer.readUInt32LE(offset + 42);
39
+ const fileNameStart = offset + 46;
40
+ const entryPath = this.buffer
41
+ .subarray(fileNameStart, fileNameStart + fileNameLength)
42
+ .toString('utf-8')
43
+ .replace(/\\/g, '/');
44
+ entries.push({
45
+ path: entryPath,
46
+ compressedSize,
47
+ uncompressedSize,
48
+ compressionMethod,
49
+ localHeaderOffset,
50
+ isDirectory: entryPath.endsWith('/'),
51
+ });
52
+ offset = fileNameStart + fileNameLength + extraLength + commentLength;
53
+ }
54
+ this.entriesCache = entries;
55
+ return entries;
56
+ }
57
+ readEntryData(entry) {
58
+ const offset = entry.localHeaderOffset;
59
+ const signature = this.buffer.readUInt32LE(offset);
60
+ if (signature !== 0x04034b50) {
61
+ throw new Error(`Invalid ZIP: bad local header signature at ${offset}`);
62
+ }
63
+ const fileNameLength = this.buffer.readUInt16LE(offset + 26);
64
+ const extraLength = this.buffer.readUInt16LE(offset + 28);
65
+ const dataStart = offset + 30 + fileNameLength + extraLength;
66
+ const compressed = this.buffer.subarray(dataStart, dataStart + entry.compressedSize);
67
+ if (entry.compressionMethod === 0) {
68
+ return Buffer.from(compressed);
69
+ }
70
+ if (entry.compressionMethod === 8) {
71
+ return inflateRawSync(compressed);
72
+ }
73
+ throw new Error(`Unsupported ZIP compression method: ${entry.compressionMethod}`);
74
+ }
75
+ findEndOfCentralDirectory() {
76
+ const minOffset = Math.max(0, this.buffer.length - 0xffff - 22);
77
+ for (let offset = this.buffer.length - 22; offset >= minOffset; offset--) {
78
+ if (this.buffer.readUInt32LE(offset) === 0x06054b50) {
79
+ return offset;
80
+ }
81
+ }
82
+ return -1;
83
+ }
84
+ }
@@ -82,5 +82,6 @@ export declare class ViteBuilder {
82
82
  /**
83
83
  * 格式化字节数
84
84
  */
85
+ private runComplianceValidation;
85
86
  private formatBytes;
86
87
  }
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { ViteConfigBuilder } from './vite/config-builder.js';
5
5
  import { GenericViteConfigBuilder } from './vite/config-builder-generic.js';
6
6
  import { PLATFORM_CONFIGS } from './vite/platform-configs.js';
7
+ import { formatValidationSummary, hasBlockingValidationErrors, runPlayableValidation, saveValidationReport, } from './validation/index.js';
7
8
  /**
8
9
  * Vite 构建器 - 使用 Vite 构建 Playable Ads
9
10
  *
@@ -90,6 +91,7 @@ export class ViteBuilder {
90
91
  // 6. 验证输出大小
91
92
  const outputPath = this.getOutputPath();
92
93
  await this.validateSize(outputPath);
94
+ await this.runComplianceValidation(outputPath, metadata);
93
95
  // 7. 生成报告
94
96
  this.generateReport(outputPath);
95
97
  return outputPath;
@@ -350,6 +352,47 @@ export class ViteBuilder {
350
352
  /**
351
353
  * 格式化字节数
352
354
  */
355
+ async runComplianceValidation(outputPath, metadata) {
356
+ const validationOptions = this.options.validation;
357
+ if (validationOptions?.enabled === false) {
358
+ return;
359
+ }
360
+ if (this.engine !== 'phaser') {
361
+ return;
362
+ }
363
+ const isZip = outputPath.toLowerCase().endsWith('.zip');
364
+ const stages = validationOptions?.stages
365
+ ?? (isZip ? ['delivery-package'] : ['html-artifact']);
366
+ const report = await runPlayableValidation({
367
+ projectRoot: this.baseBuildDir,
368
+ channel: this.options.platform,
369
+ engine: this.engine,
370
+ buildTool: metadata?.buildTool,
371
+ buildChain: metadata?.buildTool === 'playable-scripts'
372
+ ? 'devkit'
373
+ : metadata?.buildTool === 'playcanvas-native'
374
+ ? 'playcanvas-native'
375
+ : 'generic',
376
+ htmlPath: isZip ? undefined : outputPath,
377
+ packagePath: isZip ? outputPath : undefined,
378
+ outputDir: this.options.outputDir,
379
+ sourceGlobs: validationOptions?.sourceGlobs,
380
+ metadata: metadata ? { ...metadata } : undefined,
381
+ }, stages);
382
+ const summary = formatValidationSummary(report);
383
+ if (report.summary.status === 'pass') {
384
+ console.log(summary);
385
+ }
386
+ else {
387
+ console.warn(summary);
388
+ }
389
+ if (validationOptions?.reportPath) {
390
+ await saveValidationReport(report, validationOptions.reportPath);
391
+ }
392
+ if (validationOptions?.failOnError && hasBlockingValidationErrors(report)) {
393
+ throw new Error(`Playable validation failed for ${this.options.platform}`);
394
+ }
395
+ }
353
396
  formatBytes(bytes) {
354
397
  if (bytes === 0)
355
398
  return '0 Bytes';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcraft/build",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -32,7 +32,7 @@
32
32
  "scripts": {
33
33
  "dev": "tsc -w",
34
34
  "build": "tsc",
35
- "release": "node scripts/release.js"
35
+ "release": "node ../../scripts/release-npm.mjs --packages=build --skip-install --skip-tests --skip-git-push"
36
36
  },
37
37
  "dependencies": {
38
38
  "@playcraft/ads-tracking": "^1.0.0",