@playcraft/devkit 1.0.31-beta.10 → 1.0.31-beta.11

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.
Files changed (92) hide show
  1. package/README.md +262 -0
  2. package/cli/bin/playable-scripts.js +53 -6
  3. package/cli/commands/build.js +241 -32
  4. package/cli/commands/builds.js +13 -2
  5. package/cli/commands/dev.js +22 -1
  6. package/cli/commands/pack.js +320 -40
  7. package/cli/commands/vite-dev.js +7 -1
  8. package/core/artifacts/emit.js +98 -1
  9. package/core/artifacts/fflate.js +31 -1
  10. package/core/artifacts/zip.js +39 -1
  11. package/core/batch-build.js +1261 -147
  12. package/core/constants.js +20 -2
  13. package/core/cos-uploader.js +289 -23
  14. package/core/engines/cocos/clean-cache.js +179 -5
  15. package/core/engines/cocos/clean-cache.test.js +75 -1
  16. package/core/engines/cocos/cli-build.js +116 -2
  17. package/core/engines/cocos/cli.js +38 -1
  18. package/core/engines/cocos/constants.js +21 -1
  19. package/core/engines/cocos/creator-cli.js +108 -1
  20. package/core/engines/cocos/dev-server.js +412 -4
  21. package/core/engines/cocos/fs.js +105 -1
  22. package/core/engines/cocos/injects/3x/init.js +438 -19
  23. package/core/engines/cocos/injects/3x/main.js +52 -2
  24. package/core/engines/cocos/injects/libs/pako.js +2 -1
  25. package/core/engines/cocos/mcp-refresh.js +76 -2
  26. package/core/engines/cocos/merge.js +147 -2
  27. package/core/engines/cocos/pack.js +187 -1
  28. package/core/engines/cocos/preview-cache.js +48 -2
  29. package/core/engines/cocos/preview-cache.test.js +32 -1
  30. package/core/engines/cocos/preview-concat.js +189 -5
  31. package/core/engines/cocos/preview-concat.test.js +73 -1
  32. package/core/engines/cocos/preview-json-hook.js +200 -1
  33. package/core/engines/cocos/preview-json-pack.js +376 -2
  34. package/core/engines/cocos/preview-json-pack.test.js +226 -2
  35. package/core/engines/cocos/preview-proxy.js +132 -1
  36. package/core/engines/cocos/rebundle-preview.js +54 -2
  37. package/core/engines/cocos/strip-social-share.js +115 -3
  38. package/core/engines/cocos/theme-assets.js +161 -2
  39. package/core/engines/cocos/writable-cli.js +248 -2
  40. package/core/engines/cocos/writable-cli.test.js +112 -1
  41. package/core/engines/web/index.js +20 -1
  42. package/core/engines/web/vite.dev.js +322 -17
  43. package/core/engines/web/webpack.build.js +522 -65
  44. package/core/engines/web/webpack.common.js +177 -11
  45. package/core/engines/web/webpack.dev.js +88 -6
  46. package/core/index.js +101 -7
  47. package/core/injectors/apply.js +94 -1
  48. package/core/injectors/check-channel-matrix.js +130 -2
  49. package/core/injectors/spec.js +217 -9
  50. package/core/loaders/gltf-loader.js +74 -28
  51. package/core/options.js +353 -16
  52. package/core/plugins/AdikteevInjectorPlugin.js +26 -4
  53. package/core/plugins/BigoAdsInjectorPlugin.js +21 -4
  54. package/core/plugins/DAPIInjectorPlugin.js +25 -2
  55. package/core/plugins/DebuggerInjectionPlugin.js +31 -3
  56. package/core/plugins/ExitAPIInjectorPlugin.js +56 -3
  57. package/core/plugins/FflateCompressionPlugin.js +231 -40
  58. package/core/plugins/FontInjectionWebpackPlugin.js +46 -4
  59. package/core/plugins/LiftoffInjectorPlugin.js +26 -4
  60. package/core/plugins/MRAIDInjectorPlugin.js +24 -2
  61. package/core/plugins/MintegralInjectorPlugin.js +25 -2
  62. package/core/plugins/PangleInjectorPlugin.js +24 -2
  63. package/core/plugins/PlaycraftOrientationInjectorPlugin.js +29 -3
  64. package/core/plugins/SnapchatInjectorPlugin.js +26 -4
  65. package/core/plugins/SocialMediaInjectorPlugin.js +119 -7
  66. package/core/plugins/TikTokInjectorPlugin.js +24 -2
  67. package/core/plugins/UnityInjectorPlugin.js +37 -8
  68. package/core/plugins/ZipPlugin.js +154 -20
  69. package/core/plugins/socialMediaVitePlugin.js +108 -2
  70. package/core/utils/buildDefines.js +30 -2
  71. package/core/utils/buildTemplateString.js +39 -5
  72. package/core/utils/date.js +16 -1
  73. package/core/utils/fontRegistry.js +81 -13
  74. package/core/utils/fontResolver.js +335 -37
  75. package/core/utils/generateAdikteevHtmlWebpackPluginConfig.js +42 -7
  76. package/core/utils/generateBigabidHtmlWebpackPluginConfig.js +30 -3
  77. package/core/utils/generateInMobiHtmlWebpackPluginConfig.js +43 -7
  78. package/core/utils/getPlaycraftOrientationSuffix.js +9 -2
  79. package/core/utils/injectSDKPlugins.js +68 -13
  80. package/core/utils/logOptions.js +46 -5
  81. package/core/utils/mergeOptions.js +21 -2
  82. package/core/utils/parseArgvOptions.js +125 -5
  83. package/core/utils/resolveChannelFold2Zip.js +15 -3
  84. package/core/utils/validateThemeData.js +227 -26
  85. package/core/validators/pre-build-checker.js +287 -25
  86. package/core/validators/tracking-validator.js +441 -40
  87. package/core/validators/tracking-validator.test.js +84 -2
  88. package/core/vite.dev.js +1 -1
  89. package/core/webpack.build.js +1 -1
  90. package/core/webpack.common.js +1 -1
  91. package/core/webpack.dev.js +1 -1
  92. package/package.json +3 -2
@@ -1 +1,187 @@
1
- "use strict";const fs=require("fs");const path=require("path");const{options}=require("../../options.js");const{mergeOptions}=require("../../utils/mergeOptions.js");const{logOptions}=require("../../utils/logOptions.js");const{resolveCreatorOutputDir}=require("./creator-cli");const{runCocosEngineBuild}=require("./cli-build");const{rebundlePreviewMerge}=require("./rebundle-preview.js");const{mergeWebMobile}=require("./merge");const{detectCurrentTheme,loadThemePayload,writeGeneratedThemeModule}=require("./theme-assets");const{emitChannel}=require("../../artifacts/emit");const{isSocialMediaNetwork,stripSocialShareFromDir}=require("./strip-social-share");function resolveWebMobileDir(root,buildOptions){const cocos=buildOptions.cocos||{};const fromCreator=resolveCreatorOutputDir(root,cocos.buildConfig||"build-configs/web-mobile.json");const explicit=cocos.webMobileDir?path.resolve(root,cocos.webMobileDir):null;if(cocos.skipEngineBuild){return explicit||fromCreator||path.resolve(root,"dist/web-mobile")}if(fromCreator){if(explicit&&explicit!==fromCreator){console.warn(`[cocos] webMobileDir (${path.relative(root,explicit)}) != Creator output (${path.relative(root,fromCreator)}); using Creator buildPath/outputName`)}return fromCreator}return explicit||path.resolve(root,"dist/web-mobile")}async function mergeTemplate(root,buildOptions){const webMobileDir=resolveWebMobileDir(root,buildOptions);const singleFilePath=path.join(root,"build","single-file-3x.html");await mergeWebMobile(webMobileDir,singleFilePath,{enableSplash:!!(buildOptions.cocos&&buildOptions.cocos.enableSplash)});return fs.readFileSync(singleFilePath,"utf8")}function resolveThemeName(root,buildOptions,themeConfig){return buildOptions.themeName||detectCurrentTheme(root,themeConfig)||"default"}function loadBuildsThemeConfig(root){const configPath=path.join(root,"builds.config.js");if(!fs.existsSync(configPath))return{sourceDir:"src/theme",entryFile:"src/theme/index.ts"};try{delete require.cache[configPath];const config=require(configPath);return{sourceDir:"src/theme",entryFile:"src/theme/index.ts",...(config.themes||{})}}catch{return{sourceDir:"src/theme",entryFile:"src/theme/index.ts"}}}function stripPackedChannel(outDir,network){if(isSocialMediaNetwork(network)){console.log("[cocos] social_media: keep AiX hooks (inject IIFE later)");return}const packed=stripSocialShareFromDir(outDir);console.log(`[cocos] stripped social-share from ${packed.changed}/${packed.files} files in ${path.relative(process.cwd(),outDir)||"."}`)}async function prepareCocosTemplate(customOptions){const buildOptions=mergeOptions(options,customOptions);const root=process.cwd();await runCocosEngineBuild(root,buildOptions);await rebundlePreviewMerge(root);return mergeTemplate(root,buildOptions)}async function emitCocosChannel(templateHtml,customOptions){const buildOptions=mergeOptions(options,customOptions);const root=process.cwd();const themeConfig=loadBuildsThemeConfig(root);const themeName=resolveThemeName(root,buildOptions,themeConfig);let themePayload={themeName,mergedData:{},i18nOverride:{},assetRecordByPath:{}};try{themePayload=loadThemePayload(root,themeName,themeConfig)}catch(err){console.warn(`[cocos] theme payload skipped: ${err.message}`)}const generated=buildOptions.cocos&&buildOptions.cocos.generatedThemeFile||"assets/scripts/platform/theme.generated.ts";writeGeneratedThemeModule(path.join(root,generated),themePayload);const outDir=path.resolve(root,buildOptions.outDir);await emitChannel({templateHtml,buildOptions,themePayload,outDir});stripPackedChannel(outDir,buildOptions.network)}async function runCocosBuild(customOptions){const buildOptions=mergeOptions(options,customOptions);logOptions(buildOptions);const templateHtml=await prepareCocosTemplate(buildOptions);await emitCocosChannel(templateHtml,customOptions)}async function packCocosBatch({config,themes,tasks,outputDir,projectRoot,isChannelFold2Zip}){const buildOptionsBase=mergeOptions(options,{engine:"cocos",googlePlayUrl:config.googlePlayUrl,appStoreUrl:config.appStoreUrl,isChannelFold2Zip,obfuscateLevel:config.obfuscateLevel,fflateCompression:config.fflateCompression});if(config.cocos)Object.assign(buildOptionsBase.cocos,config.cocos);if(buildOptionsBase.cocos.cleanCache!==false&&process.env.PLAYABLE_KEEP_COCOS_CACHE!=="1"){buildOptionsBase.cocos.cleanCache=true}await runCocosEngineBuild(projectRoot,buildOptionsBase);await rebundlePreviewMerge(projectRoot);const templateHtml=await mergeTemplate(projectRoot,buildOptionsBase);const themeConfig=config.themes||loadBuildsThemeConfig(projectRoot);const results=[];for(const theme of themes){let themePayload;try{themePayload=loadThemePayload(projectRoot,theme,themeConfig)}catch(err){console.error(`[cocos] theme ${theme}: ${err.message}`);for(const task of tasks){results.push({success:false,theme,channel:task.channel,orientation:task.orientation,error:err.message})}continue}for(const task of tasks){const orientationValue=task.orientation||"both";const fileName=config.naming&&typeof config.naming.generator==="function"?config.naming.generator(theme,task.channel):`${theme}-${task.channel}`;const tempDir=path.join(projectRoot,`${config.build.tempDirPrefix||"dist-temp"}-cocos-${theme}-${task.channel}-${orientationValue}`);try{const channelOptions=mergeOptions(buildOptionsBase,{network:task.channel,playcraftOrientationValue:orientationValue,filename:fileName||options.filename,outDir:tempDir,isChannelFold2Zip});await emitChannel({templateHtml,buildOptions:channelOptions,themePayload,outDir:tempDir});stripPackedChannel(tempDir,task.channel);const themeDir=path.join(outputDir,theme);fs.mkdirSync(themeDir,{recursive:true});for(const file of fs.readdirSync(tempDir)){const src=path.join(tempDir,file);if(fs.statSync(src).isDirectory())continue;fs.copyFileSync(src,path.join(themeDir,file))}results.push({success:true,theme,channel:task.channel,orientation:orientationValue})}catch(error){results.push({success:false,theme,channel:task.channel,orientation:orientationValue,error:error.message})}finally{if(config.build.cleanTemp!==false&&fs.existsSync(tempDir)){fs.rmSync(tempDir,{recursive:true,force:true})}}}}return results}async function syncThemeForPreview(projectRoot,themeName,themeConfig,generatedRel){const payload=loadThemePayload(projectRoot,themeName,themeConfig);writeGeneratedThemeModule(path.join(projectRoot,generatedRel),payload);return payload}module.exports={runCocosBuild,prepareCocosTemplate,emitCocosChannel,packCocosBatch,mergeTemplate,syncThemeForPreview};
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { options } = require('../../options.js');
6
+ const { mergeOptions } = require('../../utils/mergeOptions.js');
7
+ const { logOptions } = require('../../utils/logOptions.js');
8
+ const { resolveCreatorOutputDir } = require('./creator-cli');
9
+ const { runCocosEngineBuild } = require('./cli-build');
10
+ const { rebundlePreviewMerge } = require('./rebundle-preview.js');
11
+ const { mergeWebMobile } = require('./merge');
12
+ const { detectCurrentTheme, loadThemePayload, writeGeneratedThemeModule } = require('./theme-assets');
13
+ const { emitChannel } = require('../../artifacts/emit');
14
+ const { isSocialMediaNetwork, stripSocialShareFromDir } = require('./strip-social-share');
15
+
16
+ function resolveWebMobileDir(root, buildOptions) {
17
+ const cocos = buildOptions.cocos || {};
18
+ const fromCreator = resolveCreatorOutputDir(root, cocos.buildConfig || 'build-configs/web-mobile.json');
19
+ const explicit = cocos.webMobileDir ? path.resolve(root, cocos.webMobileDir) : null;
20
+ if (cocos.skipEngineBuild) {
21
+ return explicit || fromCreator || path.resolve(root, 'dist/web-mobile');
22
+ }
23
+ if (fromCreator) {
24
+ if (explicit && explicit !== fromCreator) {
25
+ console.warn(
26
+ `[cocos] webMobileDir (${path.relative(root, explicit)}) != Creator output (${path.relative(root, fromCreator)}); using Creator buildPath/outputName`
27
+ );
28
+ }
29
+ return fromCreator;
30
+ }
31
+ return explicit || path.resolve(root, 'dist/web-mobile');
32
+ }
33
+
34
+ async function mergeTemplate(root, buildOptions) {
35
+ const webMobileDir = resolveWebMobileDir(root, buildOptions);
36
+ const singleFilePath = path.join(root, 'build', 'single-file-3x.html');
37
+ await mergeWebMobile(webMobileDir, singleFilePath, {
38
+ enableSplash: !!(buildOptions.cocos && buildOptions.cocos.enableSplash),
39
+ });
40
+ return fs.readFileSync(singleFilePath, 'utf8');
41
+ }
42
+
43
+ function resolveThemeName(root, buildOptions, themeConfig) {
44
+ return buildOptions.themeName || detectCurrentTheme(root, themeConfig) || 'default';
45
+ }
46
+
47
+ function loadBuildsThemeConfig(root) {
48
+ const configPath = path.join(root, 'builds.config.js');
49
+ if (!fs.existsSync(configPath)) return { sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' };
50
+ try {
51
+ delete require.cache[configPath];
52
+ const config = require(configPath);
53
+ return { sourceDir: 'src/theme', entryFile: 'src/theme/index.ts', ...(config.themes || {}) };
54
+ } catch {
55
+ return { sourceDir: 'src/theme', entryFile: 'src/theme/index.ts' };
56
+ }
57
+ }
58
+
59
+ function stripPackedChannel(outDir, network) {
60
+ if (isSocialMediaNetwork(network)) {
61
+ console.log('[cocos] social_media: keep AiX hooks (inject IIFE later)');
62
+ return;
63
+ }
64
+ const packed = stripSocialShareFromDir(outDir);
65
+ console.log(`[cocos] stripped social-share from ${packed.changed}/${packed.files} files in ${path.relative(process.cwd(), outDir) || '.'}`);
66
+ }
67
+
68
+ async function prepareCocosTemplate(customOptions) {
69
+ const buildOptions = mergeOptions(options, customOptions);
70
+ const root = process.cwd();
71
+ await runCocosEngineBuild(root, buildOptions);
72
+ await rebundlePreviewMerge(root);
73
+ return mergeTemplate(root, buildOptions);
74
+ }
75
+
76
+ async function emitCocosChannel(templateHtml, customOptions) {
77
+ const buildOptions = mergeOptions(options, customOptions);
78
+ const root = process.cwd();
79
+ const themeConfig = loadBuildsThemeConfig(root);
80
+ const themeName = resolveThemeName(root, buildOptions, themeConfig);
81
+ let themePayload = { themeName, mergedData: {}, i18nOverride: {}, assetRecordByPath: {} };
82
+ try {
83
+ themePayload = loadThemePayload(root, themeName, themeConfig);
84
+ } catch (err) {
85
+ console.warn(`[cocos] theme payload skipped: ${err.message}`);
86
+ }
87
+ const generated = (buildOptions.cocos && buildOptions.cocos.generatedThemeFile) || 'assets/scripts/platform/theme.generated.ts';
88
+ writeGeneratedThemeModule(path.join(root, generated), themePayload);
89
+ const outDir = path.resolve(root, buildOptions.outDir);
90
+ await emitChannel({ templateHtml, buildOptions, themePayload, outDir });
91
+ stripPackedChannel(outDir, buildOptions.network);
92
+ }
93
+
94
+ async function runCocosBuild(customOptions) {
95
+ const buildOptions = mergeOptions(options, customOptions);
96
+ logOptions(buildOptions);
97
+ const templateHtml = await prepareCocosTemplate(buildOptions);
98
+ await emitCocosChannel(templateHtml, customOptions);
99
+ }
100
+
101
+ async function packCocosBatch({ config, themes, tasks, outputDir, projectRoot, isChannelFold2Zip }) {
102
+ const buildOptionsBase = mergeOptions(options, {
103
+ engine: 'cocos',
104
+ googlePlayUrl: config.googlePlayUrl,
105
+ appStoreUrl: config.appStoreUrl,
106
+ isChannelFold2Zip,
107
+ obfuscateLevel: config.obfuscateLevel,
108
+ fflateCompression: config.fflateCompression,
109
+ });
110
+ if (config.cocos) Object.assign(buildOptionsBase.cocos, config.cocos);
111
+ if (buildOptionsBase.cocos.cleanCache !== false && process.env.PLAYABLE_KEEP_COCOS_CACHE !== '1') {
112
+ buildOptionsBase.cocos.cleanCache = true;
113
+ }
114
+
115
+ await runCocosEngineBuild(projectRoot, buildOptionsBase);
116
+ await rebundlePreviewMerge(projectRoot);
117
+ const templateHtml = await mergeTemplate(projectRoot, buildOptionsBase);
118
+ const themeConfig = config.themes || loadBuildsThemeConfig(projectRoot);
119
+ const results = [];
120
+
121
+ for (const theme of themes) {
122
+ let themePayload;
123
+ try {
124
+ themePayload = loadThemePayload(projectRoot, theme, themeConfig);
125
+ } catch (err) {
126
+ console.error(`[cocos] theme ${theme}: ${err.message}`);
127
+ for (const task of tasks) {
128
+ results.push({ success: false, theme, channel: task.channel, orientation: task.orientation, error: err.message });
129
+ }
130
+ continue;
131
+ }
132
+
133
+ for (const task of tasks) {
134
+ const orientationValue = task.orientation || 'both';
135
+ const fileName = (config.naming && typeof config.naming.generator === 'function')
136
+ ? config.naming.generator(theme, task.channel)
137
+ : `${theme}-${task.channel}`;
138
+ const tempDir = path.join(projectRoot, `${config.build.tempDirPrefix || 'dist-temp'}-cocos-${theme}-${task.channel}-${orientationValue}`);
139
+ try {
140
+ const channelOptions = mergeOptions(buildOptionsBase, {
141
+ network: task.channel,
142
+ playcraftOrientationValue: orientationValue,
143
+ filename: fileName || options.filename,
144
+ outDir: tempDir,
145
+ isChannelFold2Zip,
146
+ });
147
+ await emitChannel({
148
+ templateHtml,
149
+ buildOptions: channelOptions,
150
+ themePayload,
151
+ outDir: tempDir,
152
+ });
153
+ stripPackedChannel(tempDir, task.channel);
154
+ const themeDir = path.join(outputDir, theme);
155
+ fs.mkdirSync(themeDir, { recursive: true });
156
+ for (const file of fs.readdirSync(tempDir)) {
157
+ const src = path.join(tempDir, file);
158
+ if (fs.statSync(src).isDirectory()) continue;
159
+ fs.copyFileSync(src, path.join(themeDir, file));
160
+ }
161
+ results.push({ success: true, theme, channel: task.channel, orientation: orientationValue });
162
+ } catch (error) {
163
+ results.push({ success: false, theme, channel: task.channel, orientation: orientationValue, error: error.message });
164
+ } finally {
165
+ if (config.build.cleanTemp !== false && fs.existsSync(tempDir)) {
166
+ fs.rmSync(tempDir, { recursive: true, force: true });
167
+ }
168
+ }
169
+ }
170
+ }
171
+ return results;
172
+ }
173
+
174
+ async function syncThemeForPreview(projectRoot, themeName, themeConfig, generatedRel) {
175
+ const payload = loadThemePayload(projectRoot, themeName, themeConfig);
176
+ writeGeneratedThemeModule(path.join(projectRoot, generatedRel), payload);
177
+ return payload;
178
+ }
179
+
180
+ module.exports = {
181
+ runCocosBuild,
182
+ prepareCocosTemplate,
183
+ emitCocosChannel,
184
+ packCocosBatch,
185
+ mergeTemplate,
186
+ syncThemeForPreview,
187
+ };
@@ -1,5 +1,51 @@
1
- "use strict";const IMMUTABLE="public, max-age=31536000, immutable";const REVALIDATE="public, max-age=0, must-revalidate";const NO_STORE="no-store";function pathOnly(url){return String(url||"").split("?")[0]}function hasContentHash(url){return /[?&]h=[a-f0-9]{6,}/i.test(String(url||""))}/**
1
+ 'use strict';
2
+
3
+ const IMMUTABLE = 'public, max-age=31536000, immutable';
4
+ const REVALIDATE = 'public, max-age=0, must-revalidate';
5
+ const NO_STORE = 'no-store';
6
+
7
+ function pathOnly(url) {
8
+ return String(url || '').split('?')[0];
9
+ }
10
+
11
+ function hasContentHash(url) {
12
+ return /[?&]h=[a-f0-9]{6,}/i.test(String(url || ''));
13
+ }
14
+
15
+ /**
2
16
  * Cache-Control for CLI preview. Engine / static never change during a remix
3
17
  * session; hashed playcraft artifacts are immutable until the next compile.
4
18
  * HTML / settings / MCP stay no-store.
5
- */function cacheControlFor(url){const pathname=pathOnly(url);if(!pathname||pathname==="/")return NO_STORE;if(pathname.endsWith(".html"))return NO_STORE;if(pathname==="/preview/settings.js")return NO_STORE;if(pathname.startsWith("/mcp"))return NO_STORE;if(pathname.startsWith("/console-log"))return NO_STORE;if(pathname.startsWith("/socket.io/")&&!pathname.endsWith(".js"))return null;if(/^\/scene\/.+\.json$/.test(pathname))return NO_STORE;if(pathname.startsWith("/scripting/engine-dist/"))return IMMUTABLE;if(pathname.startsWith("/static/web/"))return IMMUTABLE;if(pathname.startsWith("/scripting/engine/"))return IMMUTABLE;if(pathname==="/socket.io/socket.io.js")return IMMUTABLE;if(/\/chunks\/[0-9a-f]{2}\/[0-9a-f]{40}\.js$/.test(pathname))return IMMUTABLE;if(hasContentHash(url)&&(pathname.endsWith("/playcraft-bundle.js")||pathname.endsWith("/playcraft-json-pack.json"))){return IMMUTABLE}return REVALIDATE}module.exports={cacheControlFor,IMMUTABLE,REVALIDATE,NO_STORE};
19
+ */
20
+ function cacheControlFor(url) {
21
+ const pathname = pathOnly(url);
22
+ if (!pathname || pathname === '/') return NO_STORE;
23
+ if (pathname.endsWith('.html')) return NO_STORE;
24
+ if (pathname === '/preview/settings.js') return NO_STORE;
25
+ if (pathname.startsWith('/mcp')) return NO_STORE;
26
+ if (pathname.startsWith('/console-log')) return NO_STORE;
27
+ if (pathname.startsWith('/socket.io/') && !pathname.endsWith('.js')) return null;
28
+ if (/^\/scene\/.+\.json$/.test(pathname)) return NO_STORE;
29
+
30
+ if (pathname.startsWith('/scripting/engine-dist/')) return IMMUTABLE;
31
+ if (pathname.startsWith('/static/web/')) return IMMUTABLE;
32
+ if (pathname.startsWith('/scripting/engine/')) return IMMUTABLE;
33
+ if (pathname === '/socket.io/socket.io.js') return IMMUTABLE;
34
+ if (/\/chunks\/[0-9a-f]{2}\/[0-9a-f]{40}\.js$/.test(pathname)) return IMMUTABLE;
35
+
36
+ if (
37
+ hasContentHash(url) &&
38
+ (pathname.endsWith('/playcraft-bundle.js') || pathname.endsWith('/playcraft-json-pack.json'))
39
+ ) {
40
+ return IMMUTABLE;
41
+ }
42
+
43
+ return REVALIDATE;
44
+ }
45
+
46
+ module.exports = {
47
+ cacheControlFor,
48
+ IMMUTABLE,
49
+ REVALIDATE,
50
+ NO_STORE,
51
+ };
@@ -1 +1,32 @@
1
- "use strict";const{describe,it}=require("node:test");const assert=require("node:assert/strict");const{cacheControlFor,IMMUTABLE,REVALIDATE,NO_STORE}=require("./preview-cache.js");describe("cacheControlFor",()=>{it("does not cache html, settings, or mcp",()=>{assert.equal(cacheControlFor("/"),NO_STORE);assert.equal(cacheControlFor("/preview/settings.js"),NO_STORE);assert.equal(cacheControlFor("/mcp"),NO_STORE);assert.equal(cacheControlFor("/scene/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d.json"),NO_STORE)});it("long-caches engine and static files that do not change",()=>{assert.equal(cacheControlFor("/scripting/engine-dist/bundled/index.js"),IMMUTABLE);assert.equal(cacheControlFor("/static/web/polyfills.bundle.js"),IMMUTABLE);assert.equal(cacheControlFor("/scripting/engine/effect-settings"),IMMUTABLE)});it("long-caches hashed playcraft artifacts and revalidates unhashed ones",()=>{assert.equal(cacheControlFor("/scripting/x/chunks/playcraft-bundle.js?h=abcdef12"),IMMUTABLE);assert.equal(cacheControlFor("/scripting/x/chunks/playcraft-bundle.js"),REVALIDATE);assert.equal(cacheControlFor("/scripting/systemjs/playcraft-json-pack.json?h=abcdef12"),IMMUTABLE)})});
1
+ 'use strict';
2
+
3
+ const { describe, it } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { cacheControlFor, IMMUTABLE, REVALIDATE, NO_STORE } = require('./preview-cache.js');
6
+
7
+ describe('cacheControlFor', () => {
8
+ it('does not cache html, settings, or mcp', () => {
9
+ assert.equal(cacheControlFor('/'), NO_STORE);
10
+ assert.equal(cacheControlFor('/preview/settings.js'), NO_STORE);
11
+ assert.equal(cacheControlFor('/mcp'), NO_STORE);
12
+ assert.equal(cacheControlFor('/scene/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d.json'), NO_STORE);
13
+ });
14
+
15
+ it('long-caches engine and static files that do not change', () => {
16
+ assert.equal(cacheControlFor('/scripting/engine-dist/bundled/index.js'), IMMUTABLE);
17
+ assert.equal(cacheControlFor('/static/web/polyfills.bundle.js'), IMMUTABLE);
18
+ assert.equal(cacheControlFor('/scripting/engine/effect-settings'), IMMUTABLE);
19
+ });
20
+
21
+ it('long-caches hashed playcraft artifacts and revalidates unhashed ones', () => {
22
+ assert.equal(
23
+ cacheControlFor('/scripting/x/chunks/playcraft-bundle.js?h=abcdef12'),
24
+ IMMUTABLE,
25
+ );
26
+ assert.equal(cacheControlFor('/scripting/x/chunks/playcraft-bundle.js'), REVALIDATE);
27
+ assert.equal(
28
+ cacheControlFor('/scripting/systemjs/playcraft-json-pack.json?h=abcdef12'),
29
+ IMMUTABLE,
30
+ );
31
+ });
32
+ });
@@ -1,8 +1,192 @@
1
- "use strict";const crypto=require("crypto");const fs=require("fs");const path=require("path");const ENTRY="cce:/internal/x/prerequisite-imports";const SKIP=new Set(["cce:/internal/x/cc"]);const GLOBAL_DEPS=new Set(["cc","cc/env","cc/userland/macro"]);const BUNDLE_FILE="playcraft-bundle.js";const BUNDLE_URL=`./chunks/${BUNDLE_FILE}`;const REGISTER_RE=/^System\.register\(\s*(\[[\s\S]*?\])\s*,\s*function/;function previewDir(projectRoot){return path.join(projectRoot,"temp/programming/packer-driver/targets/preview")}function normalizeChunkUrl(url){const raw=String(url||"").replace(/\\/g,"/");if(raw.startsWith("./chunks/"))return raw;if(raw.startsWith("chunks/"))return`./${raw}`;return raw}function isChunkUrl(url){return normalizeChunkUrl(url).startsWith("./chunks/")&&normalizeChunkUrl(url).endsWith(".js")}function packChunkName(chunkUrl){return`pack-chunk:${path.basename(normalizeChunkUrl(chunkUrl),".js")}`}function shortHash(buf){return crypto.createHash("sha1").update(buf).digest("hex").slice(0,10)}function isBundledImportMap(map){const imports=map&&map.imports||{};const entry=String(imports[ENTRY]||"").split("?")[0];return entry===BUNDLE_URL&&Object.keys(imports).length===1}function loadSourceImportMap(dir){const livePath=path.join(dir,"import-map.json");const backupPath=path.join(dir,"import-map.quickpack.json");if(!fs.existsSync(livePath))return null;const live=JSON.parse(fs.readFileSync(livePath,"utf8"));if(isBundledImportMap(live)){if(!fs.existsSync(backupPath))return null;return JSON.parse(fs.readFileSync(backupPath,"utf8"))}fs.writeFileSync(backupPath,`${JSON.stringify(live,null,2)}\n`);return live}function skipChunkUrls(map){const skip=new Set;const imports=map.imports||{};for(const spec of SKIP){if(isChunkUrl(imports[spec]))skip.add(normalizeChunkUrl(imports[spec]))}return skip}function collectChunkUrls(map){const skipUrls=skipChunkUrls(map);const urls=new Set;const queue=[];const add=url=>{if(!isChunkUrl(url))return;const normalized=normalizeChunkUrl(url);if(skipUrls.has(normalized)||urls.has(normalized))return;urls.add(normalized);queue.push(normalized)};const imports=map.imports||{};for(const[spec,url]of Object.entries(imports)){if(SKIP.has(spec))continue;add(url)}while(queue.length){const scopeUrl=queue.shift();for(const target of Object.values((map.scopes||{})[scopeUrl]||{})){add(target)}}return urls}function rewriteDep(dep,chunkUrl,map,nameOf){if(GLOBAL_DEPS.has(dep))return dep;const mapped=((map.scopes||{})[chunkUrl]||{})[dep]||(map.imports||{})[dep];if(!mapped)return dep;if(typeof mapped==="string"&&mapped.startsWith("cce:/internal/x/cc-fu"))return"cc";if(SKIP.has(mapped)||mapped===(map.imports||{})["cce:/internal/x/cc"])return"cc";if(isChunkUrl(mapped))return nameOf(normalizeChunkUrl(mapped));return mapped}function rewriteChunk(source,name,chunkUrl,map,nameOf,anonymous){const stripped=source.replace(/\n\/\/# sourceMappingURL=.*$/m,"");const match=stripped.match(REGISTER_RE);if(!match){throw new Error(`no System.register in ${chunkUrl}`)}const deps=JSON.parse(match[1]).map(dep=>rewriteDep(dep,chunkUrl,map,nameOf));const head=anonymous?`System.register(${JSON.stringify(deps)}, function`:`System.register(${JSON.stringify(name)}, ${JSON.stringify(deps)}, function`;return`${head}${stripped.slice(match[0].length)}`}/**
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const ENTRY = 'cce:/internal/x/prerequisite-imports';
8
+ const SKIP = new Set(['cce:/internal/x/cc']);
9
+ const GLOBAL_DEPS = new Set(['cc', 'cc/env', 'cc/userland/macro']);
10
+ const BUNDLE_FILE = 'playcraft-bundle.js';
11
+ const BUNDLE_URL = `./chunks/${BUNDLE_FILE}`;
12
+ const REGISTER_RE = /^System\.register\(\s*(\[[\s\S]*?\])\s*,\s*function/;
13
+
14
+ function previewDir(projectRoot) {
15
+ return path.join(projectRoot, 'temp/programming/packer-driver/targets/preview');
16
+ }
17
+
18
+ function normalizeChunkUrl(url) {
19
+ const raw = String(url || '').replace(/\\/g, '/');
20
+ if (raw.startsWith('./chunks/')) return raw;
21
+ if (raw.startsWith('chunks/')) return `./${raw}`;
22
+ return raw;
23
+ }
24
+
25
+ function isChunkUrl(url) {
26
+ return normalizeChunkUrl(url).startsWith('./chunks/') && normalizeChunkUrl(url).endsWith('.js');
27
+ }
28
+
29
+ function packChunkName(chunkUrl) {
30
+ return `pack-chunk:${path.basename(normalizeChunkUrl(chunkUrl), '.js')}`;
31
+ }
32
+
33
+ function shortHash(buf) {
34
+ return crypto.createHash('sha1').update(buf).digest('hex').slice(0, 10);
35
+ }
36
+
37
+ function isBundledImportMap(map) {
38
+ const imports = (map && map.imports) || {};
39
+ const entry = String(imports[ENTRY] || '').split('?')[0];
40
+ return entry === BUNDLE_URL && Object.keys(imports).length === 1;
41
+ }
42
+
43
+ function loadSourceImportMap(dir) {
44
+ const livePath = path.join(dir, 'import-map.json');
45
+ const backupPath = path.join(dir, 'import-map.quickpack.json');
46
+ if (!fs.existsSync(livePath)) return null;
47
+ const live = JSON.parse(fs.readFileSync(livePath, 'utf8'));
48
+ if (isBundledImportMap(live)) {
49
+ if (!fs.existsSync(backupPath)) return null;
50
+ return JSON.parse(fs.readFileSync(backupPath, 'utf8'));
51
+ }
52
+ fs.writeFileSync(backupPath, `${JSON.stringify(live, null, 2)}\n`);
53
+ return live;
54
+ }
55
+
56
+ function skipChunkUrls(map) {
57
+ const skip = new Set();
58
+ const imports = map.imports || {};
59
+ for (const spec of SKIP) {
60
+ if (isChunkUrl(imports[spec])) skip.add(normalizeChunkUrl(imports[spec]));
61
+ }
62
+ return skip;
63
+ }
64
+
65
+ function collectChunkUrls(map) {
66
+ const skipUrls = skipChunkUrls(map);
67
+ const urls = new Set();
68
+ const queue = [];
69
+ const add = (url) => {
70
+ if (!isChunkUrl(url)) return;
71
+ const normalized = normalizeChunkUrl(url);
72
+ if (skipUrls.has(normalized) || urls.has(normalized)) return;
73
+ urls.add(normalized);
74
+ queue.push(normalized);
75
+ };
76
+ const imports = map.imports || {};
77
+ for (const [spec, url] of Object.entries(imports)) {
78
+ if (SKIP.has(spec)) continue;
79
+ add(url);
80
+ }
81
+ while (queue.length) {
82
+ const scopeUrl = queue.shift();
83
+ for (const target of Object.values((map.scopes || {})[scopeUrl] || {})) {
84
+ add(target);
85
+ }
86
+ }
87
+ return urls;
88
+ }
89
+
90
+ function rewriteDep(dep, chunkUrl, map, nameOf) {
91
+ if (GLOBAL_DEPS.has(dep)) return dep;
92
+ const mapped =
93
+ ((map.scopes || {})[chunkUrl] || {})[dep] ||
94
+ (map.imports || {})[dep];
95
+ if (!mapped) return dep;
96
+ if (typeof mapped === 'string' && mapped.startsWith('cce:/internal/x/cc-fu')) return 'cc';
97
+ if (SKIP.has(mapped) || mapped === (map.imports || {})['cce:/internal/x/cc']) return 'cc';
98
+ if (isChunkUrl(mapped)) return nameOf(normalizeChunkUrl(mapped));
99
+ return mapped;
100
+ }
101
+
102
+ function rewriteChunk(source, name, chunkUrl, map, nameOf, anonymous) {
103
+ const stripped = source.replace(/\n\/\/# sourceMappingURL=.*$/m, '');
104
+ const match = stripped.match(REGISTER_RE);
105
+ if (!match) {
106
+ throw new Error(`no System.register in ${chunkUrl}`);
107
+ }
108
+ const deps = JSON.parse(match[1]).map((dep) => rewriteDep(dep, chunkUrl, map, nameOf));
109
+ const head = anonymous
110
+ ? `System.register(${JSON.stringify(deps)}, function`
111
+ : `System.register(${JSON.stringify(name)}, ${JSON.stringify(deps)}, function`;
112
+ return `${head}${stripped.slice(match[0].length)}`;
113
+ }
114
+
115
+ /**
2
116
  * Fold QuickPack's per-file chunks into one named-register bundle and
3
117
  * point prerequisite-imports at it. Original chunks stay on disk for
4
118
  * incremental compile; import-map.quickpack.json keeps the pre-rewrite map.
5
- */function concatPreviewScripts(projectRoot){const dir=previewDir(projectRoot);const map=loadSourceImportMap(dir);if(!map)return null;const imports=map.imports||{};const chunkToSpecifier=new Map;for(const[spec,url]of Object.entries(imports)){if(SKIP.has(spec)||!isChunkUrl(url))continue;chunkToSpecifier.set(normalizeChunkUrl(url),spec)}const entryChunk=imports[ENTRY]&&isChunkUrl(imports[ENTRY])?normalizeChunkUrl(imports[ENTRY]):null;const nameOf=chunkUrl=>{if(entryChunk&&chunkUrl===entryChunk)return ENTRY;return chunkToSpecifier.get(chunkUrl)||packChunkName(chunkUrl)};const chunkUrls=[...collectChunkUrls(map)];if(!chunkUrls.length)return null;// Named modules first; anonymous entry last so script-tag onload getRegister()
6
- // binds prerequisite-imports (named-register's firstNamedDefine is cleared
7
- // before the load event).
8
- const ordered=[];for(const url of chunkUrls){if(url!==entryChunk)ordered.push(url)}if(entryChunk&&chunkUrls.includes(entryChunk))ordered.push(entryChunk);const parts=[];for(const chunkUrl of ordered){const abs=path.join(dir,chunkUrl.replace(/^\.\//,""));if(!fs.existsSync(abs)){throw new Error(`missing chunk ${chunkUrl}`)}parts.push(rewriteChunk(fs.readFileSync(abs,"utf8"),nameOf(chunkUrl),chunkUrl,map,nameOf,chunkUrl===entryChunk))}const bundle=`${parts.join("\n")}\n`;const bundleAbs=path.join(dir,"chunks",BUNDLE_FILE);fs.mkdirSync(path.dirname(bundleAbs),{recursive:true});fs.writeFileSync(bundleAbs,bundle);const hashedUrl=`${BUNDLE_URL}?h=${shortHash(bundle)}`;const bundledMap={imports:{[ENTRY]:hashedUrl}};const tmp=path.join(dir,"import-map.json.tmp");fs.writeFileSync(tmp,`${JSON.stringify(bundledMap)}\n`);fs.renameSync(tmp,path.join(dir,"import-map.json"));return{modules:ordered.length,bytes:Buffer.byteLength(bundle),bundle:hashedUrl}}module.exports={concatPreviewScripts,ENTRY,BUNDLE_URL,BUNDLE_FILE,isBundledImportMap};
119
+ */
120
+ function concatPreviewScripts(projectRoot) {
121
+ const dir = previewDir(projectRoot);
122
+ const map = loadSourceImportMap(dir);
123
+ if (!map) return null;
124
+
125
+ const imports = map.imports || {};
126
+ const chunkToSpecifier = new Map();
127
+ for (const [spec, url] of Object.entries(imports)) {
128
+ if (SKIP.has(spec) || !isChunkUrl(url)) continue;
129
+ chunkToSpecifier.set(normalizeChunkUrl(url), spec);
130
+ }
131
+
132
+ const entryChunk = imports[ENTRY] && isChunkUrl(imports[ENTRY])
133
+ ? normalizeChunkUrl(imports[ENTRY])
134
+ : null;
135
+ const nameOf = (chunkUrl) => {
136
+ if (entryChunk && chunkUrl === entryChunk) return ENTRY;
137
+ return chunkToSpecifier.get(chunkUrl) || packChunkName(chunkUrl);
138
+ };
139
+ const chunkUrls = [...collectChunkUrls(map)];
140
+ if (!chunkUrls.length) return null;
141
+ // Named modules first; anonymous entry last so script-tag onload getRegister()
142
+ // binds prerequisite-imports (named-register's firstNamedDefine is cleared
143
+ // before the load event).
144
+ const ordered = [];
145
+ for (const url of chunkUrls) {
146
+ if (url !== entryChunk) ordered.push(url);
147
+ }
148
+ if (entryChunk && chunkUrls.includes(entryChunk)) ordered.push(entryChunk);
149
+
150
+ const parts = [];
151
+ for (const chunkUrl of ordered) {
152
+ const abs = path.join(dir, chunkUrl.replace(/^\.\//, ''));
153
+ if (!fs.existsSync(abs)) {
154
+ throw new Error(`missing chunk ${chunkUrl}`);
155
+ }
156
+ parts.push(
157
+ rewriteChunk(
158
+ fs.readFileSync(abs, 'utf8'),
159
+ nameOf(chunkUrl),
160
+ chunkUrl,
161
+ map,
162
+ nameOf,
163
+ chunkUrl === entryChunk,
164
+ ),
165
+ );
166
+ }
167
+
168
+ const bundle = `${parts.join('\n')}\n`;
169
+ const bundleAbs = path.join(dir, 'chunks', BUNDLE_FILE);
170
+ fs.mkdirSync(path.dirname(bundleAbs), { recursive: true });
171
+ fs.writeFileSync(bundleAbs, bundle);
172
+
173
+ const hashedUrl = `${BUNDLE_URL}?h=${shortHash(bundle)}`;
174
+ const bundledMap = { imports: { [ENTRY]: hashedUrl } };
175
+ const tmp = path.join(dir, 'import-map.json.tmp');
176
+ fs.writeFileSync(tmp, `${JSON.stringify(bundledMap)}\n`);
177
+ fs.renameSync(tmp, path.join(dir, 'import-map.json'));
178
+
179
+ return {
180
+ modules: ordered.length,
181
+ bytes: Buffer.byteLength(bundle),
182
+ bundle: hashedUrl,
183
+ };
184
+ }
185
+
186
+ module.exports = {
187
+ concatPreviewScripts,
188
+ ENTRY,
189
+ BUNDLE_URL,
190
+ BUNDLE_FILE,
191
+ isBundledImportMap,
192
+ };
@@ -1 +1,73 @@
1
- "use strict";const{describe,it,before,after}=require("node:test");const assert=require("node:assert/strict");const fs=require("fs");const os=require("os");const path=require("path");const{concatPreviewScripts,ENTRY}=require("./preview-concat.js");describe("concatPreviewScripts",()=>{let root;let preview;before(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),"preview-concat-"));preview=path.join(root,"temp/programming/packer-driver/targets/preview");fs.mkdirSync(path.join(preview,"chunks/aa"),{recursive:true});fs.mkdirSync(path.join(preview,"chunks/bb"),{recursive:true});fs.writeFileSync(path.join(preview,"chunks/aa/aaa111.js"),"System.register([\"__unresolved_0\", \"cc\", \"@playcraft/adsdk/cjs\"], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n//# sourceMappingURL=aaa111.js.map\n");fs.writeFileSync(path.join(preview,"chunks/bb/bbb222.js"),"System.register([], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n");fs.writeFileSync(path.join(preview,"import-map.json"),JSON.stringify({imports:{[ENTRY]:"./chunks/aa/aaa111.js","cce:/internal/x/cc":"./chunks/cc/skip-me.js"},scopes:{"./chunks/aa/aaa111.js":{__unresolved_0:"./chunks/bb/bbb222.js","@playcraft/adsdk/cjs":"./chunks/bb/bbb222.js",cc:"./chunks/cc/skip-me.js"}}}))});after(()=>{fs.rmSync(root,{recursive:true,force:true})});it("writes one named-register bundle and a single import-map entry",()=>{const result=concatPreviewScripts(root);assert.equal(result.modules,2);const map=JSON.parse(fs.readFileSync(path.join(preview,"import-map.json"),"utf8"));assert.match(map.imports[ENTRY],/^\.\/chunks\/playcraft-bundle\.js\?h=[a-f0-9]+$/);assert.equal(result.bundle,map.imports[ENTRY]);const bundle=fs.readFileSync(path.join(preview,"chunks/playcraft-bundle.js"),"utf8");assert.equal(bundle.includes("sourceMappingURL"),false);assert.equal(bundle.includes(`System.register("${ENTRY}"`),false);assert.match(bundle,/System\.register\("pack-chunk:bbb222"/);assert.match(bundle,/System\.register\(\["pack-chunk:bbb222","cc","pack-chunk:bbb222"\], function \(_export/);assert.equal(bundle.includes("\"@playcraft/adsdk/cjs\""),false);const last=bundle.trim().split("System.register").pop();assert.match(last,/^\(\[/);assert.equal(bundle.includes("functionfunction"),false);assert.equal(bundle.includes("cce:/internal/x/cc"),false)});it("is idempotent using the quickpack backup",()=>{const again=concatPreviewScripts(root);assert.equal(again.modules,2)})});
1
+ 'use strict';
2
+
3
+ const { describe, it, before, after } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+ const { concatPreviewScripts, ENTRY } = require('./preview-concat.js');
9
+
10
+ describe('concatPreviewScripts', () => {
11
+ let root;
12
+ let preview;
13
+
14
+ before(() => {
15
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'preview-concat-'));
16
+ preview = path.join(root, 'temp/programming/packer-driver/targets/preview');
17
+ fs.mkdirSync(path.join(preview, 'chunks/aa'), { recursive: true });
18
+ fs.mkdirSync(path.join(preview, 'chunks/bb'), { recursive: true });
19
+ fs.writeFileSync(
20
+ path.join(preview, 'chunks/aa/aaa111.js'),
21
+ 'System.register(["__unresolved_0", "cc", "@playcraft/adsdk/cjs"], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n//# sourceMappingURL=aaa111.js.map\n',
22
+ );
23
+ fs.writeFileSync(
24
+ path.join(preview, 'chunks/bb/bbb222.js'),
25
+ 'System.register([], function (_export, _context) {\n return { setters: [], execute() {} };\n});\n',
26
+ );
27
+ fs.writeFileSync(
28
+ path.join(preview, 'import-map.json'),
29
+ JSON.stringify({
30
+ imports: {
31
+ [ENTRY]: './chunks/aa/aaa111.js',
32
+ 'cce:/internal/x/cc': './chunks/cc/skip-me.js',
33
+ },
34
+ scopes: {
35
+ './chunks/aa/aaa111.js': {
36
+ __unresolved_0: './chunks/bb/bbb222.js',
37
+ '@playcraft/adsdk/cjs': './chunks/bb/bbb222.js',
38
+ cc: './chunks/cc/skip-me.js',
39
+ },
40
+ },
41
+ }),
42
+ );
43
+ });
44
+
45
+ after(() => {
46
+ fs.rmSync(root, { recursive: true, force: true });
47
+ });
48
+
49
+ it('writes one named-register bundle and a single import-map entry', () => {
50
+ const result = concatPreviewScripts(root);
51
+ assert.equal(result.modules, 2);
52
+
53
+ const map = JSON.parse(fs.readFileSync(path.join(preview, 'import-map.json'), 'utf8'));
54
+ assert.match(map.imports[ENTRY], /^\.\/chunks\/playcraft-bundle\.js\?h=[a-f0-9]+$/);
55
+ assert.equal(result.bundle, map.imports[ENTRY]);
56
+
57
+ const bundle = fs.readFileSync(path.join(preview, 'chunks/playcraft-bundle.js'), 'utf8');
58
+ assert.equal(bundle.includes('sourceMappingURL'), false);
59
+ assert.equal(bundle.includes(`System.register("${ENTRY}"`), false);
60
+ assert.match(bundle, /System\.register\("pack-chunk:bbb222"/);
61
+ assert.match(bundle, /System\.register\(\["pack-chunk:bbb222","cc","pack-chunk:bbb222"\], function \(_export/);
62
+ assert.equal(bundle.includes('"@playcraft/adsdk/cjs"'), false);
63
+ const last = bundle.trim().split('System.register').pop();
64
+ assert.match(last, /^\(\[/);
65
+ assert.equal(bundle.includes('functionfunction'), false);
66
+ assert.equal(bundle.includes('cce:/internal/x/cc'), false);
67
+ });
68
+
69
+ it('is idempotent using the quickpack backup', () => {
70
+ const again = concatPreviewScripts(root);
71
+ assert.equal(again.modules, 2);
72
+ });
73
+ });