emsdk-env 0.8.0 → 0.9.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.
@@ -1,11 +1,11 @@
1
1
  /*!
2
2
  * name: emsdk-env
3
- * version: 0.8.0
3
+ * version: 0.9.0
4
4
  * description: Emscripten environment builder
5
5
  * author: Kouji Matsui (@kekyo@mi.kekyo.net)
6
6
  * license: MIT
7
7
  * repository.url: https://github.com/kekyo/emsdk-env
8
- * git.commit.hash: 8c3f97866a85cc6c99383eff3526c04077032eac
8
+ * git.commit.hash: 73d7e217903f020ef22daa9289362dcd19bddac1
9
9
  */
10
10
 
11
11
  "use strict";
@@ -575,6 +575,11 @@ const DEFAULT_IMPORT_LIB_DIR = "lib";
575
575
  const DEFAULT_WASM_BUILD_DIR = path.join(os.tmpdir(), "emsdk-env");
576
576
  const DEFAULT_EMSDK_TARGET_VERSION = "latest";
577
577
  const DEFAULT_WASM_OPT_ARGS = ["-Oz"];
578
+ const DEFAULT_GENERATED_LOADER_OUT_FILE = path.join(
579
+ "src",
580
+ "generated",
581
+ "wasm-loader.ts"
582
+ );
578
583
  let buildSequence = 0;
579
584
  const padNumber = (value, length = 2) => String(value).padStart(length, "0");
580
585
  const formatTimestamp = (date) => {
@@ -601,7 +606,7 @@ const normalizePrepareOptions = (options) => {
601
606
  ...rest
602
607
  };
603
608
  };
604
- const parseKeyValueInput = (values) => {
609
+ const parseStringKeyValueInput = (values) => {
605
610
  const parsed = {};
606
611
  for (const entry of values) {
607
612
  const index = entry.indexOf("=");
@@ -615,24 +620,40 @@ const parseKeyValueInput = (values) => {
615
620
  }
616
621
  return parsed;
617
622
  };
618
- const isKeyValueMap = (value) => value instanceof Map;
619
- const normalizeKeyValueInput = (input) => {
623
+ const isDefineMap = (input) => input instanceof Map;
624
+ const normalizeDefineInput = (input) => {
625
+ if (!input) {
626
+ return {};
627
+ }
628
+ if (Array.isArray(input)) {
629
+ return parseStringKeyValueInput(input);
630
+ }
631
+ if (isDefineMap(input)) {
632
+ return Object.fromEntries(input);
633
+ }
634
+ return { ...input };
635
+ };
636
+ const isLinkDirectiveMap = (input) => input instanceof Map;
637
+ const normalizeLinkDirectiveInput = (input) => {
620
638
  if (!input) {
621
639
  return {};
622
640
  }
623
641
  if (Array.isArray(input)) {
624
- return parseKeyValueInput(input);
642
+ return parseStringKeyValueInput(input);
625
643
  }
626
- if (isKeyValueMap(input)) {
644
+ if (isLinkDirectiveMap(input)) {
627
645
  return Object.fromEntries(input);
628
646
  }
629
647
  return { ...input };
630
648
  };
631
649
  const mergeDefines = (common, target) => ({
632
- ...normalizeKeyValueInput(common),
633
- ...normalizeKeyValueInput(target)
650
+ ...normalizeDefineInput(common),
651
+ ...normalizeDefineInput(target)
652
+ });
653
+ const mergeLinkDirectives = (common, target) => ({
654
+ ...normalizeLinkDirectiveInput(common),
655
+ ...normalizeLinkDirectiveInput(target)
634
656
  });
635
- const mergeLinkDirectives = (common, target) => mergeDefines(common, target);
636
657
  const resolveWasmOptEnabled = (common, target) => {
637
658
  var _a, _b;
638
659
  return (_b = (_a = target == null ? void 0 : target.enable) != null ? _a : common == null ? void 0 : common.enable) != null ? _b : false;
@@ -716,14 +737,25 @@ const resolveDefines = (defines, env) => {
716
737
  }
717
738
  return resolved;
718
739
  };
740
+ const resolveLinkDirectiveValue = (value, env, label) => {
741
+ if (typeof value === "string") {
742
+ return expandPlaceholders(value, env, label);
743
+ }
744
+ if (Array.isArray(value)) {
745
+ return value.map(
746
+ (entry, index) => expandPlaceholders(entry, env, `${label}[${index}]`)
747
+ );
748
+ }
749
+ return value;
750
+ };
719
751
  const resolveLinkDirectives = (directives, env) => {
720
752
  const resolved = {};
721
753
  for (const [key, value] of Object.entries(directives)) {
722
- if (typeof value === "string") {
723
- resolved[key] = expandPlaceholders(value, env, `linkDirectives.${key}`);
724
- } else {
725
- resolved[key] = value;
726
- }
754
+ resolved[key] = resolveLinkDirectiveValue(
755
+ value,
756
+ env,
757
+ `linkDirectives.${key}`
758
+ );
727
759
  }
728
760
  return resolved;
729
761
  };
@@ -748,12 +780,13 @@ const resolveSourcesFromPatterns = async (patterns, env, srcDir, label) => {
748
780
  const buildDefineFlags = (defines) => Object.entries(defines).flatMap(
749
781
  ([key, value]) => value === null || value === void 0 ? [`-D${key}`] : [`-D${key}=${String(value)}`]
750
782
  );
783
+ const serializeLinkDirectiveValue = (value) => Array.isArray(value) ? JSON.stringify(value) : String(value);
751
784
  const buildLinkDirectiveFlags = (directives) => {
752
785
  if (Object.keys(directives).length === 0) {
753
786
  return [];
754
787
  }
755
788
  return Object.entries(directives).flatMap(
756
- ([key, value]) => value === null || value === void 0 ? ["-s", key] : ["-s", `${key}=${String(value)}`]
789
+ ([key, value]) => value === null || value === void 0 ? ["-s", key] : ["-s", `${key}=${serializeLinkDirectiveValue(value)}`]
757
790
  );
758
791
  };
759
792
  const buildExportFlags = (exports$1) => {
@@ -808,6 +841,248 @@ const dedupeValues = (values) => {
808
841
  }
809
842
  return deduped;
810
843
  };
844
+ const isSubPath = (parentDir, targetPath) => {
845
+ const rel = path.relative(parentDir, targetPath);
846
+ if (rel === "") {
847
+ return true;
848
+ }
849
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
850
+ };
851
+ const readTextIfExists = async (filePath) => {
852
+ try {
853
+ return await promises.readFile(filePath, "utf8");
854
+ } catch (error) {
855
+ const nodeError = error;
856
+ if (nodeError.code === "ENOENT") {
857
+ return void 0;
858
+ }
859
+ throw error;
860
+ }
861
+ };
862
+ const writeTextIfChanged = async (filePath, content) => {
863
+ const existing = await readTextIfExists(filePath);
864
+ if (existing === content) {
865
+ return false;
866
+ }
867
+ await ensureDirectory(path.dirname(filePath));
868
+ await promises.writeFile(filePath, content, "utf8");
869
+ return true;
870
+ };
871
+ const toPascalCaseIdentifier = (value) => {
872
+ const tokens = value.split(/[^A-Za-z0-9]+/).map((token) => token.trim()).filter((token) => token.length > 0);
873
+ if (tokens.length === 0) {
874
+ throw new Error(`Cannot derive loader function name from target: ${value}`);
875
+ }
876
+ const pascal = tokens.map((token) => token.charAt(0).toUpperCase() + token.slice(1)).join("");
877
+ return /^[0-9]/.test(pascal) ? `Target${pascal}` : pascal;
878
+ };
879
+ const toRelativeImportSpecifier = (fromFile, toFile) => {
880
+ const rel = path.relative(path.dirname(fromFile), toFile).replace(/\\/g, "/");
881
+ return rel.startsWith(".") ? rel : `./${rel}`;
882
+ };
883
+ const buildGeneratedLoaderContent = (generatedLoaderFile, targets) => {
884
+ const targetBlocks = targets.map((target) => {
885
+ const specifier = JSON.stringify(
886
+ toRelativeImportSpecifier(generatedLoaderFile, target.outFile)
887
+ );
888
+ return `export const ${target.functionName} = async <T extends object>(
889
+ options?: TargetWasmLoadOptions
890
+ ): Promise<WasmInstance<T>> => {
891
+ const source = options?.url ?? new URL(${specifier}, import.meta.url);
892
+ return await loadWasm<T>(source, {
893
+ imports: options?.imports,
894
+ });
895
+ };`;
896
+ }).join("\n\n");
897
+ return `// Generated by emsdk-env. DO NOT EDIT.
898
+
899
+ export type WasmSource =
900
+ | string
901
+ | URL
902
+ | ArrayBuffer
903
+ | ArrayBufferView
904
+ | Response;
905
+
906
+ export interface WasmLoadOptions {
907
+ readonly imports?: WebAssembly.Imports;
908
+ }
909
+
910
+ export interface TargetWasmLoadOptions extends WasmLoadOptions {
911
+ readonly url?: string | URL;
912
+ }
913
+
914
+ export interface WasmInstance<T extends object> {
915
+ readonly exports: T;
916
+ readonly memory: WebAssembly.Memory;
917
+ readonly table: WebAssembly.Table | undefined;
918
+ readonly rawExports: WebAssembly.Exports;
919
+ readonly module: WebAssembly.Module;
920
+ readonly instance: WebAssembly.Instance;
921
+ readonly initialize: (() => unknown) | undefined;
922
+ readonly start: (() => unknown) | undefined;
923
+ }
924
+
925
+ const resolveWasmBytes = async (source: WasmSource): Promise<ArrayBuffer> => {
926
+ if (typeof Response !== 'undefined' && source instanceof Response) {
927
+ return await source.arrayBuffer();
928
+ }
929
+ if (source instanceof URL || typeof source === 'string') {
930
+ const response = await fetch(source);
931
+ if (!response.ok) {
932
+ throw new Error(\`Failed to fetch wasm: \${response.url}\`);
933
+ }
934
+ return await response.arrayBuffer();
935
+ }
936
+ if (ArrayBuffer.isView(source)) {
937
+ const view = new Uint8Array(
938
+ source.buffer,
939
+ source.byteOffset,
940
+ source.byteLength
941
+ );
942
+ return view.slice().buffer;
943
+ }
944
+ return source;
945
+ };
946
+
947
+ const getImportValue = (
948
+ imports: WebAssembly.Imports | undefined,
949
+ moduleName: string,
950
+ name: string
951
+ ) => {
952
+ const moduleImports = imports?.[moduleName];
953
+ if (!moduleImports || typeof moduleImports !== 'object') {
954
+ return undefined;
955
+ }
956
+ return (moduleImports as Record<string, unknown>)[name];
957
+ };
958
+
959
+ const resolveMemory = (
960
+ module: WebAssembly.Module,
961
+ instance: WebAssembly.Instance,
962
+ imports: WebAssembly.Imports | undefined
963
+ ) => {
964
+ let memory: WebAssembly.Memory | undefined;
965
+ for (const entry of WebAssembly.Module.exports(module)) {
966
+ if (entry.kind !== 'memory') {
967
+ continue;
968
+ }
969
+ if (memory) {
970
+ throw new Error('Multiple wasm memories are not supported.');
971
+ }
972
+ const value = instance.exports[entry.name];
973
+ if (!(value instanceof WebAssembly.Memory)) {
974
+ throw new Error(\`Export is not a WebAssembly.Memory: \${entry.name}\`);
975
+ }
976
+ memory = value;
977
+ }
978
+ if (memory) {
979
+ return memory;
980
+ }
981
+ for (const entry of WebAssembly.Module.imports(module)) {
982
+ if (entry.kind !== 'memory') {
983
+ continue;
984
+ }
985
+ if (memory) {
986
+ throw new Error('Multiple wasm memories are not supported.');
987
+ }
988
+ const value = getImportValue(imports, entry.module, entry.name);
989
+ if (!(value instanceof WebAssembly.Memory)) {
990
+ throw new Error(
991
+ \`Imported value is not a WebAssembly.Memory: \${entry.module}.\${entry.name}\`
992
+ );
993
+ }
994
+ memory = value;
995
+ }
996
+ if (!memory) {
997
+ throw new Error('WASM memory export/import was not resolved.');
998
+ }
999
+ return memory;
1000
+ };
1001
+
1002
+ const resolveTable = (
1003
+ module: WebAssembly.Module,
1004
+ instance: WebAssembly.Instance,
1005
+ imports: WebAssembly.Imports | undefined
1006
+ ) => {
1007
+ let table: WebAssembly.Table | undefined;
1008
+ for (const entry of WebAssembly.Module.exports(module)) {
1009
+ if (entry.kind !== 'table') {
1010
+ continue;
1011
+ }
1012
+ if (table) {
1013
+ throw new Error('Multiple wasm tables are not supported.');
1014
+ }
1015
+ const value = instance.exports[entry.name];
1016
+ if (!(value instanceof WebAssembly.Table)) {
1017
+ throw new Error(\`Export is not a WebAssembly.Table: \${entry.name}\`);
1018
+ }
1019
+ table = value;
1020
+ }
1021
+ if (table) {
1022
+ return table;
1023
+ }
1024
+ for (const entry of WebAssembly.Module.imports(module)) {
1025
+ if (entry.kind !== 'table') {
1026
+ continue;
1027
+ }
1028
+ if (table) {
1029
+ throw new Error('Multiple wasm tables are not supported.');
1030
+ }
1031
+ const value = getImportValue(imports, entry.module, entry.name);
1032
+ if (!(value instanceof WebAssembly.Table)) {
1033
+ throw new Error(
1034
+ \`Imported value is not a WebAssembly.Table: \${entry.module}.\${entry.name}\`
1035
+ );
1036
+ }
1037
+ table = value;
1038
+ }
1039
+ return table;
1040
+ };
1041
+
1042
+ export const loadWasm = async <T extends object>(
1043
+ source: WasmSource,
1044
+ options?: WasmLoadOptions
1045
+ ): Promise<WasmInstance<T>> => {
1046
+ const bytes = await resolveWasmBytes(source);
1047
+ const module = await WebAssembly.compile(bytes);
1048
+ const instance = await WebAssembly.instantiate(module, options?.imports ?? {});
1049
+
1050
+ const functionExports: Record<string, (...args: unknown[]) => unknown> = {};
1051
+ for (const entry of WebAssembly.Module.exports(module)) {
1052
+ if (entry.kind !== 'function') {
1053
+ continue;
1054
+ }
1055
+ const value = instance.exports[entry.name];
1056
+ if (typeof value !== 'function') {
1057
+ throw new Error(\`Export is not a function: \${entry.name}\`);
1058
+ }
1059
+ functionExports[entry.name] = value as (...args: unknown[]) => unknown;
1060
+ }
1061
+
1062
+ const initialize =
1063
+ typeof instance.exports._initialize === 'function'
1064
+ ? (instance.exports._initialize as () => unknown)
1065
+ : undefined;
1066
+ const start =
1067
+ typeof instance.exports._start === 'function'
1068
+ ? (instance.exports._start as () => unknown)
1069
+ : undefined;
1070
+
1071
+ return {
1072
+ exports: functionExports as T,
1073
+ memory: resolveMemory(module, instance, options?.imports),
1074
+ table: resolveTable(module, instance, options?.imports),
1075
+ rawExports: instance.exports,
1076
+ module,
1077
+ instance,
1078
+ initialize,
1079
+ start,
1080
+ };
1081
+ };
1082
+
1083
+ ${targetBlocks}
1084
+ `;
1085
+ };
811
1086
  const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
812
1087
  const resolvePackageJsonPath = async (startPath, packageName) => {
813
1088
  let current = path.dirname(startPath);
@@ -908,6 +1183,42 @@ const resolveImportDirectories = async (rootDir, imports) => {
908
1183
  libDirs: dedupeValues(libDirs)
909
1184
  };
910
1185
  };
1186
+ const resolveGeneratedLoaderOutFile = (rootDir, env, generatedLoader) => {
1187
+ var _a;
1188
+ if (!(generatedLoader == null ? void 0 : generatedLoader.enable)) {
1189
+ return void 0;
1190
+ }
1191
+ const rawOutFile = expandPlaceholders(
1192
+ (_a = generatedLoader.outFile) != null ? _a : DEFAULT_GENERATED_LOADER_OUT_FILE,
1193
+ env,
1194
+ "generatedLoader.outFile"
1195
+ );
1196
+ return resolvePath(rootDir, rawOutFile);
1197
+ };
1198
+ const resolveGeneratedLoaderWatchDirs = (rootDir, srcDir, commonIncludeDirs, env, targetEntries, importIncludeDirs) => {
1199
+ const dirs = [srcDir, ...resolveIncludeDirs(commonIncludeDirs, env, rootDir)];
1200
+ for (const [targetName, target] of targetEntries) {
1201
+ if (!target.includeDirs || target.includeDirs.length === 0) {
1202
+ continue;
1203
+ }
1204
+ const targetEnv = {
1205
+ ...env,
1206
+ TARGET_NAME: targetName
1207
+ };
1208
+ dirs.push(...resolveIncludeDirs(target.includeDirs, targetEnv, rootDir));
1209
+ }
1210
+ dirs.push(...importIncludeDirs);
1211
+ return dedupeValues(dirs);
1212
+ };
1213
+ const validateGeneratedLoaderOutFile = (generatedLoaderFile, watchDirs) => {
1214
+ for (const dir of watchDirs) {
1215
+ if (isSubPath(dir, generatedLoaderFile)) {
1216
+ throw new Error(
1217
+ `generatedLoader.outFile must not be placed under watched directory: ${generatedLoaderFile}`
1218
+ );
1219
+ }
1220
+ }
1221
+ };
911
1222
  const buildCompileArgs = (options, includeDirs, defines, env, rootDir) => {
912
1223
  const resolvedOptions = expandArray(options, env, "options");
913
1224
  const includeArgs = resolveIncludeDirs(includeDirs, env, rootDir).map(
@@ -990,6 +1301,22 @@ const buildWasm = async (options) => {
990
1301
  const importIncludeDirs = importDirectories.includeDirs;
991
1302
  const importLibDirs = importDirectories.libDirs;
992
1303
  const linkLibDirs = dedupeValues([libDir, ...importLibDirs]);
1304
+ const generatedLoaderFile = resolveGeneratedLoaderOutFile(
1305
+ rootDir,
1306
+ envWithDirs,
1307
+ options.generatedLoader
1308
+ );
1309
+ if (generatedLoaderFile) {
1310
+ const watchDirs = resolveGeneratedLoaderWatchDirs(
1311
+ rootDir,
1312
+ srcDir,
1313
+ commonIncludeDirs,
1314
+ envWithDirs,
1315
+ targetEntries,
1316
+ importIncludeDirs
1317
+ );
1318
+ validateGeneratedLoaderOutFile(generatedLoaderFile, watchDirs);
1319
+ }
993
1320
  logger.debug(`Detected rootDir: '${rootDir}'`);
994
1321
  logger.debug(`Detected srcDir: '${srcDir}'`);
995
1322
  logger.debug(`Detected outDir: '${outDir}'`);
@@ -1006,6 +1333,9 @@ const buildWasm = async (options) => {
1006
1333
  logger.debug(
1007
1334
  `Detected importLibDirs: [${importLibDirs.map((p) => `'${p}'`).join(",")}]`
1008
1335
  );
1336
+ if (generatedLoaderFile) {
1337
+ logger.debug(`Detected generatedLoaderFile: '${generatedLoaderFile}'`);
1338
+ }
1009
1339
  await ensureDirectory(outDir);
1010
1340
  await ensureDirectory(libDir);
1011
1341
  await ensureDirectory(buildDir);
@@ -1028,6 +1358,7 @@ const buildWasm = async (options) => {
1028
1358
  return wasmOptCommand;
1029
1359
  };
1030
1360
  const outFiles = {};
1361
+ let resultGeneratedLoaderFile;
1031
1362
  const buildTargets = async (expectedType) => {
1032
1363
  var _a2;
1033
1364
  for (const [targetName, target] of targetEntries) {
@@ -1299,6 +1630,40 @@ const buildWasm = async (options) => {
1299
1630
  try {
1300
1631
  await buildTargets("archive");
1301
1632
  await buildTargets("wasm");
1633
+ if (generatedLoaderFile) {
1634
+ const generatedTargets = [];
1635
+ const functionNames = /* @__PURE__ */ new Set();
1636
+ for (const [targetName, target] of targetEntries) {
1637
+ if (resolveTargetType(target.type) !== "wasm") {
1638
+ continue;
1639
+ }
1640
+ const outFile = outFiles[targetName];
1641
+ if (!outFile) {
1642
+ continue;
1643
+ }
1644
+ const functionName = `load${toPascalCaseIdentifier(targetName)}Wasm`;
1645
+ if (functionNames.has(functionName)) {
1646
+ throw new Error(
1647
+ `Generated loader function name collision: ${functionName}`
1648
+ );
1649
+ }
1650
+ functionNames.add(functionName);
1651
+ generatedTargets.push({
1652
+ targetName,
1653
+ functionName,
1654
+ outFile
1655
+ });
1656
+ }
1657
+ const content = buildGeneratedLoaderContent(
1658
+ generatedLoaderFile,
1659
+ generatedTargets
1660
+ );
1661
+ const didWrite = await writeTextIfChanged(generatedLoaderFile, content);
1662
+ logger.info(
1663
+ didWrite ? `Generated loader: ${path.relative(rootDir, generatedLoaderFile)}` : `Generated loader unchanged: ${path.relative(rootDir, generatedLoaderFile)}`
1664
+ );
1665
+ resultGeneratedLoaderFile = generatedLoaderFile;
1666
+ }
1302
1667
  } finally {
1303
1668
  if (cleanupBuildDir) {
1304
1669
  await promises.rm(buildRunDir, { recursive: true, force: true });
@@ -1306,10 +1671,11 @@ const buildWasm = async (options) => {
1306
1671
  }
1307
1672
  return {
1308
1673
  emsdkRoot,
1309
- outFiles
1674
+ outFiles,
1675
+ ...resultGeneratedLoaderFile ? { generatedLoaderFile: resultGeneratedLoaderFile } : {}
1310
1676
  };
1311
1677
  };
1312
1678
  exports.buildWasm = buildWasm;
1313
1679
  exports.createConsoleLogger = createConsoleLogger;
1314
1680
  exports.prepareEmsdk = prepareEmsdk;
1315
- //# sourceMappingURL=build-ya4uDvN7.cjs.map
1681
+ //# sourceMappingURL=build-CgmcFNSR.cjs.map