Skip to content

Commit 4aa2057

Browse files
committed
Switch to using tar as a pack format.
This is intended to improve performance (especially when used together with @chialab/esbuild-plugin-meta-url plugin, but also generally in the browser when using many files over the "large file" limit), as well as simplify some things conceptually. As a side effect, the share root is now inspectable with common Unix tools. The generated resource.js file does require `nanotar` at runtime (and does require the use of a bundler for the browser now), but this isn't considered important since the old format was expressly designed for bundling anyways.
1 parent 88d976d commit 4aa2057

File tree

5 files changed

+84
-49
lines changed

5 files changed

+84
-49
lines changed

.github/workflows/package.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ jobs:
77
runs-on: ubuntu-latest
88
steps:
99
- name: Check out source code
10-
uses: actions/checkout@v4
10+
uses: actions/checkout@v5
1111
with:
1212
fetch-depth: 0
1313
- name: Set up node
14-
uses: actions/setup-node@v4
14+
uses: actions/setup-node@v6
1515
with:
16-
node-version: '18.x'
16+
node-version: 24
1717
- name: Prepare metadata
1818
run: node prepare.mjs
1919
- name: Install dependencies

bin/pack-resources.js

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

3-
import { readdir, readFile, writeFile, mkdir, stat } from 'fs/promises';
3+
import { readdir, readFile, writeFile, stat } from 'fs/promises';
4+
import { createTar } from 'nanotar';
45

56
async function packModules(root, urlRoot) {
67
const files = await readdir(root, { withFileTypes: true });
@@ -16,42 +17,21 @@ async function packModules(root, urlRoot) {
1617
return packedData;
1718
}
1819

19-
async function packDirectory(root, urlRoot, genRoot, dirPath = '', indent = 0) {
20-
const files = await readdir(`${root}/${dirPath}`, { withFileTypes: true });
21-
const packedData = [`{\n`];
20+
async function collectDirectory(root, dirPath = '', packedData = []) {
21+
const files = await readdir(`${root}/${dirPath}`, { withFileTypes: true });
2222
for (const file of files) {
23-
packedData.push(`${' '.repeat(indent + 1)}${JSON.stringify(file.name)}: `);
24-
const filePath = `${dirPath}/${file.name}`;
23+
const filePath = dirPath === '' ? file.name : `${dirPath}/${file.name}`;
2524
const fileStats = await stat(`${root}/${filePath}`);
2625
if (fileStats.isDirectory()) {
27-
packedData.push(await packDirectory(root, urlRoot, genRoot, filePath, indent + 1));
26+
packedData.push({name: filePath});
27+
await collectDirectory(root, filePath, packedData);
2828
} else if (fileStats.isFile()) {
29-
const fileData = await readFile(`${root}/${filePath}`);
30-
let emittedAsText = false;
31-
if (fileData.length < 131072) { // emit as a separate file if >128K
32-
try {
33-
const textData = new TextDecoder('utf-8', { fatal: true }).decode(fileData);
34-
packedData.push(JSON.stringify(textData));
35-
emittedAsText = true;
36-
} catch(e) {
37-
if (e instanceof TypeError) {
38-
emittedAsText = false;
39-
} else {
40-
throw e;
41-
}
42-
}
43-
}
44-
if (!emittedAsText) {
45-
await mkdir(`${genRoot}/${urlRoot}/${dirPath}`, { recursive: true });
46-
await writeFile(`${genRoot}/${urlRoot}/${filePath}`, fileData);
47-
packedData.push(`new URL(${JSON.stringify(urlRoot + filePath)}, import.meta.url)`);
48-
}
29+
packedData.push({name: filePath, data: await readFile(`${root}/${filePath}`)});
4930
} else {
50-
packedData.push('null');
31+
console.error(`Unsupported '${filePath}'!`);
32+
process.exit(2);
5133
}
52-
packedData.push(`,\n`);
5334
}
54-
packedData.push(`${' '.repeat(indent)}}`);
5535
return packedData;
5636
}
5737

@@ -61,22 +41,50 @@ if (!(args.length >= 2 && args.length <= 4)) {
6141
process.exit(1);
6242
}
6343

64-
const resourceFileName = args[0];
44+
const resourceFilePath = args[0];
6545
const genDirectory = args[1];
6646
const shareDirectory = args[2];
6747
const shareRoot = args[3] || 'share';
6848

69-
let output = `\
70-
export const modules = ${(await packModules(genDirectory, './')).flat(Infinity).join('')};
71-
`;
72-
if (shareDirectory)
73-
output += `\
74-
export const filesystem = {
75-
${shareRoot}: ${(await packDirectory(shareDirectory, `./${shareRoot}`, genDirectory, '', 1)).flat(Infinity).join('')}
76-
};
77-
`;
78-
else
79-
output += `\
80-
export const filesystem = {};
49+
let output = `\
50+
import { parseTar } from 'nanotar';
51+
52+
function unpackResources(url) {
53+
function defaultFetchFn(url) {
54+
return fetch(url).then((resp) => resp.arrayBuffer());
55+
}
56+
57+
return async (fetchFn = defaultFetchFn) => {
58+
const root = {};
59+
for (const tarEntry of parseTar(await fetchFn(url))) {
60+
const nameParts = tarEntry.name.split('/');
61+
const dirNames = nameParts.slice(0, -1);
62+
const fileName = nameParts[nameParts.length - 1];
63+
let dir = root;
64+
for (const dirName of dirNames)
65+
dir = dir[dirName];
66+
if (tarEntry.type === 'directory') {
67+
dir[fileName] = {};
68+
} else {
69+
dir[fileName] = tarEntry.data;
70+
}
71+
}
72+
return root;
73+
};
74+
}
75+
8176
`;
82-
await writeFile(resourceFileName, output);
77+
const moduleObject = (await packModules(genDirectory, './')).flat(Infinity).join('');
78+
output += `export const modules = ${moduleObject};\n\n`;
79+
if (shareDirectory) {
80+
const tarFilePath = resourceFilePath.replace(/\.js$/, '.tar');
81+
await writeFile(tarFilePath, createTar(await collectDirectory(shareDirectory)));
82+
const tarFileName = tarFilePath.replace(/^.+\//, '');
83+
const resourceObject = `unpackResources(new URL('./${tarFileName}', import.meta.url))`;
84+
output += `export const filesystem = {\n`;
85+
output += ` ${shareRoot}: ${resourceObject},\n`;
86+
output += `};\n`;
87+
} else {
88+
output += `export const filesystem = {};\n`;
89+
}
90+
await writeFile(resourceFilePath, output);

eslint.config.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
4+
export default [
5+
{
6+
languageOptions: {
7+
ecmaVersion: "latest",
8+
sourceType: "module",
9+
globals: {
10+
...globals.browser,
11+
...globals.node,
12+
},
13+
},
14+
rules: {
15+
...js.configs.recommended.rules,
16+
"semi": "error",
17+
"eqeqeq": "error",
18+
"no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
19+
"no-constant-condition": ["error", { checkLoops: false }],
20+
},
21+
},
22+
];

lib/api.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ async function fetchObject(obj, fetchFn) {
1313
promises.push(Promise.resolve([key, value]));
1414
} else if (value instanceof URL) {
1515
promises.push(fetchFn(value).then((fetched) => [key, fetched]));
16+
} else if (value instanceof Function) {
17+
promises.push(await value(fetchFn).then((fetched) => [key, fetched]));
1618
} else {
1719
promises.push(fetchObject(value, fetchFn).then((fetched) => [key, fetched]));
1820
}

package-in.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@yowasp/runtime",
3-
"version": "9.0",
3+
"version": "10.0",
44
"description": "Common runtime for YoWASP packages",
55
"author": "Catherine <[email protected]>",
66
"license": "ISC",
@@ -30,8 +30,11 @@
3030
"bin": {
3131
"yowasp-pack-resources": "./bin/pack-resources.js"
3232
},
33+
"dependencies": {
34+
"nanotar": "^0.2.0"
35+
},
3336
"devDependencies": {
34-
"eslint": "^8.55.0"
37+
"eslint": "^9.38.0"
3538
},
3639
"scripts": {
3740
"lint": "eslint lib"

0 commit comments

Comments
 (0)