hikkaku 0.2.0 → 0.3.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/vite/index.mjs CHANGED
@@ -1,15 +1,66 @@
1
1
  import { zip } from "fflate";
2
- import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import * as path from "node:path";
4
- import { pathToFileURL } from "node:url";
4
+ import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { createServerModuleRunner } from "vite";
6
+ import crypto from "node:crypto";
6
7
 
8
+ //#region src/vite/plugin-scratch-import.ts
9
+ const pluginScratchImport = () => ({
10
+ name: "vite-plugin-hikkaku:scratch-import",
11
+ enforce: "pre",
12
+ resolveId(source, importer, _options) {
13
+ if (source.endsWith("?scratch")) {
14
+ if (source.endsWith(".svg?scratch") || source.endsWith(".png?scratch") || source.endsWith(".wav?scratch") || source.endsWith(".mp3?scratch")) {
15
+ const importerPath = pathToFileURL(importer ?? "");
16
+ return { id: `\0scratch:${new URL(source, importerPath)}` };
17
+ }
18
+ }
19
+ },
20
+ async load(id, _options) {
21
+ if (id.startsWith("\0scratch:")) {
22
+ const url = new URL(id.slice(9, -8));
23
+ const ext = url.pathname.split(".").pop();
24
+ if (!ext) throw new Error(`Unsupported scratch asset type: ${url.pathname}`);
25
+ const file = await readFile(fileURLToPath(url));
26
+ const hash = crypto.createHash("md5");
27
+ hash.update(file);
28
+ const md5 = hash.digest("hex");
29
+ const data = {
30
+ name: path.basename(url.pathname),
31
+ _data: Buffer.from(file).toString("base64"),
32
+ assetId: md5,
33
+ dataFormat: ext,
34
+ md5ext: `${md5}.${ext}`
35
+ };
36
+ return `
37
+ const data = ${JSON.stringify(data)}
38
+ // to Uint8Array
39
+ data._data = Uint8Array.from(atob(data._data), c => c.charCodeAt(0));
40
+
41
+ export default data
42
+ `;
43
+ }
44
+ }
45
+ });
46
+
47
+ //#endregion
7
48
  //#region src/vite/index.ts
8
49
  const BASE_URL = "https://scratchfoundation.github.io/scratch-gui/";
9
50
  const VIRTUAL_MODULE_IDS = { project: "/@virtual/hikkaku-project" };
10
51
  function hikkaku(init) {
11
52
  let runner = null;
12
- return {
53
+ let additionalAssets = /* @__PURE__ */ new Map();
54
+ const assetCache = /* @__PURE__ */ new Map();
55
+ const setContentType = (res, assetId) => {
56
+ const assetExt = path.extname(assetId).toLowerCase();
57
+ if (assetExt === ".png") res.setHeader("Content-Type", "image/png");
58
+ else if (assetExt === ".jpg" || assetExt === ".jpeg") res.setHeader("Content-Type", "image/jpeg");
59
+ else if (assetExt === ".wav") res.setHeader("Content-Type", "audio/wav");
60
+ else if (assetExt === ".mp3") res.setHeader("Content-Type", "audio/mpeg");
61
+ else res.setHeader("Content-Type", "application/octet-stream");
62
+ };
63
+ return [{
13
64
  name: "vite-plugin-hikkaku",
14
65
  config(config, env) {
15
66
  if (env.command === "build") (config.plugins?.find((p) => p && typeof p === "object" && "name" in p && p.name === "vite-plugin-turbowarp-packager"))?.api.setEntry(path.join(process.cwd(), "dist", "project.sb3"));
@@ -38,8 +89,12 @@ function hikkaku(init) {
38
89
  }
39
90
  const { default: project } = await import(pathToFileURL(path.join(process.cwd(), "dist/.tmp", "project.mjs")).href);
40
91
  const projectJSON = project.toScratch();
92
+ const assets = project.getAdditionalAssets();
41
93
  const zipData = await new Promise((resolve, reject) => {
42
- zip({ "project.json": new TextEncoder().encode(JSON.stringify(projectJSON)) }, (err, data) => {
94
+ zip({
95
+ "project.json": new TextEncoder().encode(JSON.stringify(projectJSON)),
96
+ ...Object.fromEntries(assets.entries())
97
+ }, (err, data) => {
43
98
  if (err) reject(err);
44
99
  else resolve(data);
45
100
  });
@@ -56,6 +111,12 @@ function hikkaku(init) {
56
111
  name: "project.json",
57
112
  source: JSON.stringify(projectJSON, null, 2)
58
113
  });
114
+ for (const [assetId, data] of assets.entries()) this.emitFile({
115
+ type: "asset",
116
+ fileName: `assets/${assetId}`,
117
+ name: `assets/${assetId}`,
118
+ source: data
119
+ });
59
120
  await rm(tmpDir, {
60
121
  recursive: true,
61
122
  force: true
@@ -88,6 +149,7 @@ function hikkaku(init) {
88
149
  if (this.environment.name !== "hikkaku") return;
89
150
  if (!runner) throw new Error("Module runner is not initialized.");
90
151
  const project = (await runner.import(init.entry)).default;
152
+ additionalAssets = project.getAdditionalAssets();
91
153
  options.server.environments.client.hot.send("hikkaku:project", project.toScratch());
92
154
  },
93
155
  async configureServer(server) {
@@ -98,9 +160,54 @@ function hikkaku(init) {
98
160
  server.environments.client.hot.on("vite:client:connect", async () => {
99
161
  if (!runner) throw new Error("Module runner is not initialized.");
100
162
  const project = (await runner.import(init.entry)).default;
163
+ additionalAssets = project.getAdditionalAssets();
101
164
  server.environments.client.hot.send("hikkaku:project", project.toScratch());
102
165
  });
103
166
  server.middlewares.use(async (req, res, next) => {
167
+ if (req.url?.startsWith("/hikkaku-assets/")) {
168
+ const segments = req.url.split("/");
169
+ const assetId = segments[segments.indexOf("hikkaku-assets") + 1];
170
+ if (!assetId) {
171
+ res.statusCode = 400;
172
+ res.end("Asset ID is required");
173
+ return;
174
+ }
175
+ const assetData = additionalAssets.get(assetId);
176
+ if (!assetData) {
177
+ if (assetCache.has(assetId)) {
178
+ const cached = assetCache.get(assetId);
179
+ if (cached === false) {
180
+ res.statusCode = 404;
181
+ res.end("Asset not found");
182
+ return;
183
+ }
184
+ setContentType(res, assetId);
185
+ res.end(cached);
186
+ return;
187
+ }
188
+ const assetData = await fetch(`https://assets.scratch.mit.edu/internalapi/asset/${assetId}/get/`).then((r) => {
189
+ if (!r.ok) return null;
190
+ return r.arrayBuffer();
191
+ }).catch((e) => {
192
+ console.warn("Failed to fetch asset from network:", e);
193
+ return null;
194
+ });
195
+ if (assetData) {
196
+ const uint8array = new Uint8Array(assetData);
197
+ assetCache.set(assetId, uint8array);
198
+ setContentType(res, assetId);
199
+ res.end(uint8array);
200
+ return;
201
+ }
202
+ assetCache.set(assetId, false);
203
+ res.statusCode = 404;
204
+ res.end("Asset not found");
205
+ return;
206
+ }
207
+ setContentType(res, assetId);
208
+ res.end(assetData);
209
+ return;
210
+ }
104
211
  if (req.url === "/") {
105
212
  const html = (await fetch(BASE_URL).then((res) => res.text())).replace("gui.js", "https://scratchfoundation.github.io/scratch-gui/gui.js").replace("</head>", "<script src=\"/@vite/client\" type=\"module\"><\/script><script type=\"module\" src=\"/@virtual/hikkaku-client\"><\/script></head>");
106
213
  res.setHeader("Content-Type", "text/html");
@@ -122,7 +229,7 @@ function hikkaku(init) {
122
229
  next();
123
230
  });
124
231
  }
125
- };
232
+ }, pluginScratchImport()];
126
233
  }
127
234
 
128
235
  //#endregion