@inlang/sdk 0.35.8 → 0.36.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.
Files changed (40) hide show
  1. package/dist/createNewProject.d.ts.map +1 -1
  2. package/dist/createNewProject.js +6 -2
  3. package/dist/createNewProject.test.js +2 -2
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/loadProject.d.ts.map +1 -1
  8. package/dist/loadProject.js +9 -1
  9. package/dist/migrations/maybeAddModuleCache.d.ts +6 -0
  10. package/dist/migrations/maybeAddModuleCache.d.ts.map +1 -0
  11. package/dist/migrations/maybeAddModuleCache.js +44 -0
  12. package/dist/resolve-modules/cache.d.ts +6 -0
  13. package/dist/resolve-modules/cache.d.ts.map +1 -0
  14. package/dist/resolve-modules/cache.js +40 -0
  15. package/dist/resolve-modules/import.d.ts +1 -11
  16. package/dist/resolve-modules/import.d.ts.map +1 -1
  17. package/dist/resolve-modules/import.js +78 -16
  18. package/dist/resolve-modules/import.test.js +3 -1
  19. package/dist/resolve-modules/resolveModules.d.ts.map +1 -1
  20. package/dist/resolve-modules/resolveModules.js +9 -11
  21. package/dist/resolve-modules/resolveModules.test.js +37 -6
  22. package/dist/resolve-modules/types.d.ts +1 -0
  23. package/dist/resolve-modules/types.d.ts.map +1 -1
  24. package/dist/v2/mocks/plural/bundle.d.ts.map +1 -1
  25. package/dist/v2/mocks/plural/bundle.js +22 -0
  26. package/dist/v2/mocks/plural/bundle.test.js +1 -1
  27. package/package.json +6 -6
  28. package/src/createNewProject.test.ts +2 -2
  29. package/src/createNewProject.ts +6 -3
  30. package/src/index.ts +8 -0
  31. package/src/loadProject.ts +11 -3
  32. package/src/migrations/maybeAddModuleCache.ts +53 -0
  33. package/src/resolve-modules/cache.ts +62 -0
  34. package/src/resolve-modules/import.test.ts +5 -1
  35. package/src/resolve-modules/import.ts +94 -24
  36. package/src/resolve-modules/resolveModules.test.ts +37 -6
  37. package/src/resolve-modules/resolveModules.ts +12 -17
  38. package/src/resolve-modules/types.ts +1 -0
  39. package/src/v2/mocks/plural/bundle.test.ts +1 -1
  40. package/src/v2/mocks/plural/bundle.ts +22 -0
@@ -1 +1 @@
1
- {"version":3,"file":"createNewProject.d.ts","sourceRoot":"","sources":["../src/createNewProject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAI1D;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC5C,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,eAAe,EAAE,eAAe,CAAA;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB"}
1
+ {"version":3,"file":"createNewProject.d.ts","sourceRoot":"","sources":["../src/createNewProject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAI1D;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAC5C,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,eAAe,EAAE,eAAe,CAAA;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAehB"}
@@ -11,7 +11,11 @@ export async function createNewProject(args) {
11
11
  if (await pathExists(args.projectPath, nodeishFs)) {
12
12
  throw new Error(`projectPath already exists, received "${args.projectPath}"`);
13
13
  }
14
- await nodeishFs.mkdir(args.projectPath, { recursive: true });
15
14
  const settingsText = JSON.stringify(args.projectSettings ?? defaultProjectSettings, undefined, 2);
16
- await nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText);
15
+ await nodeishFs.mkdir(args.projectPath, { recursive: true });
16
+ await Promise.all([
17
+ nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText),
18
+ nodeishFs.writeFile(`${args.projectPath}/.gitignore`, "cache"),
19
+ nodeishFs.mkdir(`${args.projectPath}/cache/modules`, { recursive: true }),
20
+ ]);
17
21
  }
@@ -70,14 +70,14 @@ describe("createNewProject", () => {
70
70
  const projectPath = "/test/project.inlang";
71
71
  await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
72
72
  const project = await loadProject({ projectPath, repo });
73
- expect(project.errors().length).toBe(0);
73
+ expect(project.errors()).toEqual([]);
74
74
  });
75
75
  it("should create messages inside the project directory", async () => {
76
76
  const repo = await mockRepo();
77
77
  const projectPath = "/test/project.inlang";
78
78
  await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings });
79
79
  const project = await loadProject({ projectPath, repo });
80
- expect(project.errors().length).toBe(0);
80
+ expect(project.errors()).toEqual([]);
81
81
  const testMessage = createMessage("test", { en: "test message" });
82
82
  project.query.messages.create({ data: testMessage });
83
83
  const messages = project.query.messages.getAll();
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { listProjects } from "./listProjects.js";
12
12
  export { solidAdapter, type InlangProjectWithSolidAdapter } from "./adapter/solidAdapter.js";
13
13
  export { createMessagesQuery } from "./createMessagesQuery.js";
14
14
  export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
15
+ export { ModuleError, ModuleHasNoExportsError, ModuleImportError, ModuleExportIsInvalidError, ModuleSettingsAreInvalidError, } from "./resolve-modules/errors.js";
16
+ export { randomHumanId } from "./storage/human-id/human-readable-id.js";
15
17
  export { normalizeMessage } from "./storage/helper.js";
16
18
  export * from "./messages/variant.js";
17
19
  export * from "./versionedInterfaces.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,YAAY,GACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,KAAK,6BAA6B,EAAE,MAAM,2BAA2B,CAAA;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EACN,kCAAkC,EAClC,gCAAgC,EAChC,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,aAAa,CAAA;AAEpB,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACtD,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EACX,aAAa,EACb,wBAAwB,EACxB,eAAe,EACf,eAAe,EACf,YAAY,GACZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAC9E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,YAAY,EAAE,KAAK,6BAA6B,EAAE,MAAM,2BAA2B,CAAA;AAC5F,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EACN,kCAAkC,EAClC,gCAAgC,EAChC,2BAA2B,EAC3B,uBAAuB,EACvB,uBAAuB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EACN,WAAW,EACX,uBAAuB,EACvB,iBAAiB,EACjB,0BAA0B,EAC1B,6BAA6B,GAC7B,MAAM,6BAA6B,CAAA;AAEpC,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AACtD,cAAc,uBAAuB,CAAA;AACrC,cAAc,0BAA0B,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ export { listProjects } from "./listProjects.js";
11
11
  export { solidAdapter } from "./adapter/solidAdapter.js";
12
12
  export { createMessagesQuery } from "./createMessagesQuery.js";
13
13
  export { ProjectSettingsFileJSONSyntaxError, ProjectSettingsFileNotFoundError, ProjectSettingsInvalidError, PluginLoadMessagesError, PluginSaveMessagesError, } from "./errors.js";
14
+ export { ModuleError, ModuleHasNoExportsError, ModuleImportError, ModuleExportIsInvalidError, ModuleSettingsAreInvalidError, } from "./resolve-modules/errors.js";
15
+ export { randomHumanId } from "./storage/human-id/human-readable-id.js";
14
16
  export { normalizeMessage } from "./storage/helper.js";
15
17
  export * from "./messages/variant.js";
16
18
  export * from "./versionedInterfaces.js";
@@ -1 +1 @@
1
- {"version":3,"file":"loadProject.d.ts","sourceRoot":"","sources":["../src/loadProject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EAGb,YAAY,EAGZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,4BAA4B,CAAA;AAkBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAgBhD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CAyQzB;AA+GD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
1
+ {"version":3,"file":"loadProject.d.ts","sourceRoot":"","sources":["../src/loadProject.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,aAAa,EAGb,YAAY,EAGZ,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,KAAK,cAAc,EAAkB,MAAM,4BAA4B,CAAA;AAsBhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAchD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE;IACvC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,UAAU,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,GAAG,OAAO,CAAC,aAAa,CAAC,CA+QzB;AA+GD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAQtE"}
@@ -10,8 +10,10 @@ import { migrateIfOutdated } from "@inlang/project-settings/migration";
10
10
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js";
11
11
  import { normalizePath } from "@lix-js/fs";
12
12
  import { assertValidProjectPath } from "./validateProjectPath.js";
13
+ // Migrations
13
14
  import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js";
14
15
  import { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectId.js";
16
+ import { maybeAddModuleCache } from "./migrations/maybeAddModuleCache.js";
15
17
  import { capture } from "./telemetry/capture.js";
16
18
  import { identifyProject } from "./telemetry/groupIdentify.js";
17
19
  import { stubMessagesQuery, stubMessageLintReportsQuery } from "./v2/stubQueryApi.js";
@@ -42,6 +44,7 @@ export async function loadProject(args) {
42
44
  // -- migratations ------------------------------------------------
43
45
  await maybeMigrateToDirectory({ nodeishFs, projectPath });
44
46
  await maybeCreateFirstProjectId({ projectPath, repo: args.repo });
47
+ await maybeAddModuleCache({ projectPath, repo: args.repo });
45
48
  // -- load project ------------------------------------------------------
46
49
  return await createRoot(async () => {
47
50
  // TODO remove tryCatch after https://github.com/opral/monorepo/issues/2013
@@ -103,7 +106,12 @@ export async function loadProject(args) {
103
106
  const _settings = settings();
104
107
  if (!_settings)
105
108
  return;
106
- resolveModules({ settings: _settings, nodeishFs, _import: args._import })
109
+ resolveModules({
110
+ settings: _settings,
111
+ nodeishFs,
112
+ _import: args._import,
113
+ projectPath,
114
+ })
107
115
  .then((resolvedModules) => {
108
116
  setResolvedModules(resolvedModules);
109
117
  })
@@ -0,0 +1,6 @@
1
+ import type { Repository } from "@lix-js/client";
2
+ export declare function maybeAddModuleCache(args: {
3
+ projectPath: string;
4
+ repo?: Repository;
5
+ }): Promise<void>;
6
+ //# sourceMappingURL=maybeAddModuleCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maybeAddModuleCache.d.ts","sourceRoot":"","sources":["../../src/migrations/maybeAddModuleCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAKhD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC/C,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,UAAU,CAAA;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0BhB"}
@@ -0,0 +1,44 @@
1
+ const EXPECTED_IGNORES = ["cache"];
2
+ export async function maybeAddModuleCache(args) {
3
+ if (args.repo === undefined)
4
+ return;
5
+ const projectExists = await directoryExists(args.projectPath, args.repo.nodeishFs);
6
+ if (!projectExists)
7
+ return;
8
+ const gitignorePath = args.projectPath + "/.gitignore";
9
+ const moduleCache = args.projectPath + "/cache/modules/";
10
+ const gitignoreExists = await fileExists(gitignorePath, args.repo.nodeishFs);
11
+ const moduleCacheExists = await directoryExists(moduleCache, args.repo.nodeishFs);
12
+ if (gitignoreExists) {
13
+ // non-destructively add any missing ignores
14
+ const gitignore = await args.repo.nodeishFs.readFile(gitignorePath, { encoding: "utf-8" });
15
+ const missingIgnores = EXPECTED_IGNORES.filter((ignore) => !gitignore.includes(ignore));
16
+ if (missingIgnores.length > 0) {
17
+ await args.repo.nodeishFs.appendFile(gitignorePath, "\n" + missingIgnores.join("\n"));
18
+ }
19
+ }
20
+ else {
21
+ await args.repo.nodeishFs.writeFile(gitignorePath, EXPECTED_IGNORES.join("\n"));
22
+ }
23
+ if (!moduleCacheExists) {
24
+ await args.repo.nodeishFs.mkdir(moduleCache, { recursive: true });
25
+ }
26
+ }
27
+ async function fileExists(path, nodeishFs) {
28
+ try {
29
+ const stat = await nodeishFs.stat(path);
30
+ return stat.isFile();
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function directoryExists(path, nodeishFs) {
37
+ try {
38
+ const stat = await nodeishFs.stat(path);
39
+ return stat.isDirectory();
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
@@ -0,0 +1,6 @@
1
+ import type { NodeishFilesystemSubset } from "@inlang/plugin";
2
+ /**
3
+ * Implements a "Network-First" caching strategy.
4
+ */
5
+ export declare function withCache(moduleLoader: (uri: string) => Promise<string>, projectPath: string, nodeishFs: Pick<NodeishFilesystemSubset, "readFile" | "writeFile">): (uri: string) => Promise<string>;
6
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAuC7D;;GAEG;AACH,wBAAgB,SAAS,CACxB,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EAC9C,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,IAAI,CAAC,uBAAuB,EAAE,UAAU,GAAG,WAAW,CAAC,GAChE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAelC"}
@@ -0,0 +1,40 @@
1
+ import { tryCatch } from "@inlang/result";
2
+ function escape(url) {
3
+ // collect the bytes of the UTF-8 representation
4
+ const bytes = new TextEncoder().encode(url);
5
+ // 64-bit FNV1a hash to make the file-names shorter
6
+ // https://en.wikipedia.org/wiki/FNV-1a
7
+ const hash = bytes.reduce((hash, byte) => BigInt.asUintN(64, (hash ^ BigInt(byte)) * 1099511628211n), 14695981039346656037n);
8
+ return hash.toString(36);
9
+ }
10
+ async function readModuleFromCache(moduleURI, projectPath, readFile) {
11
+ const moduleHash = escape(moduleURI);
12
+ const filePath = projectPath + `/cache/modules/${moduleHash}`;
13
+ return await tryCatch(async () => await readFile(filePath, { encoding: "utf-8" }));
14
+ }
15
+ async function writeModuleToCache(moduleURI, moduleContent, projectPath, writeFile) {
16
+ const moduleHash = escape(moduleURI);
17
+ const filePath = projectPath + `/cache/modules/${moduleHash}`;
18
+ await writeFile(filePath, moduleContent);
19
+ }
20
+ /**
21
+ * Implements a "Network-First" caching strategy.
22
+ */
23
+ export function withCache(moduleLoader, projectPath, nodeishFs) {
24
+ return async (uri) => {
25
+ const cachePromise = readModuleFromCache(uri, projectPath, nodeishFs.readFile);
26
+ const networkResult = await tryCatch(async () => await moduleLoader(uri));
27
+ if (networkResult.error) {
28
+ const cacheResult = await cachePromise;
29
+ if (!cacheResult.error)
30
+ return cacheResult.data;
31
+ else
32
+ throw networkResult.error;
33
+ }
34
+ else {
35
+ const moduleAsText = networkResult.data;
36
+ await writeModuleToCache(uri, moduleAsText, projectPath, nodeishFs.writeFile);
37
+ return moduleAsText;
38
+ }
39
+ };
40
+ }
@@ -15,15 +15,5 @@ export type ImportFunction = (uri: string) => Promise<any>;
15
15
  * const $import = createImport({ readFile: fs.readFile, fetch });
16
16
  * const module = await _import('./some-module.js');
17
17
  */
18
- export declare function createImport(args: {
19
- /** the fs from which the file can be read */
20
- readFile: NodeishFilesystemSubset["readFile"];
21
- }): (uri: string) => ReturnType<typeof $import>;
22
- declare function $import(uri: string, options: {
23
- /**
24
- * Required to import from a local path.
25
- */
26
- readFile: NodeishFilesystemSubset["readFile"];
27
- }): Promise<any>;
28
- export {};
18
+ export declare function createImport(projectPath: string, nodeishFs: Pick<NodeishFilesystemSubset, "readFile" | "writeFile">): (uri: string) => Promise<any>;
29
19
  //# sourceMappingURL=import.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/import.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAG7D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IAClC,6CAA6C;IAC7C,QAAQ,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAA;CAC7C,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,UAAU,CAAC,OAAO,OAAO,CAAC,CAG9C;AAED,iBAAe,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;IACR;;OAEG;IACH,QAAQ,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAA;CAC7C,GACC,OAAO,CAAC,GAAG,CAAC,CAyBd"}
1
+ {"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/import.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAA;AAK7D;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC3B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,IAAI,CAAC,uBAAuB,EAAE,UAAU,GAAG,WAAW,CAAC,SAErD,MAAM,kBACnB"}
@@ -1,5 +1,7 @@
1
1
  import dedent from "dedent";
2
2
  import { ModuleImportError } from "./errors.js";
3
+ import { withCache } from "./cache.js";
4
+ import { tryCatch } from "@inlang/result";
3
5
  /**
4
6
  * Creates the import function.
5
7
  *
@@ -9,20 +11,13 @@ import { ModuleImportError } from "./errors.js";
9
11
  * const $import = createImport({ readFile: fs.readFile, fetch });
10
12
  * const module = await _import('./some-module.js');
11
13
  */
12
- export function createImport(args) {
13
- // resembles a native import api
14
- return (uri) => $import(uri, args);
14
+ export function createImport(projectPath, nodeishFs) {
15
+ return (uri) => $import(uri, projectPath, nodeishFs);
15
16
  }
16
- async function $import(uri, options) {
17
- let moduleAsText;
18
- if (uri.startsWith("http")) {
19
- moduleAsText = await (await fetch(uri)).text();
20
- }
21
- else {
22
- moduleAsText = await options.readFile(uri, {
23
- encoding: "utf-8",
24
- });
25
- }
17
+ async function $import(uri, projectPath, nodeishFs) {
18
+ const moduleAsText = uri.startsWith("http")
19
+ ? await withCache(readModuleFromCDN, projectPath, nodeishFs)(uri)
20
+ : await readModulefromDisk(uri, nodeishFs.readFile);
26
21
  const moduleWithMimeType = "data:application/javascript," + encodeURIComponent(moduleAsText);
27
22
  try {
28
23
  return await import(/* @vite-ignore */ moduleWithMimeType);
@@ -30,11 +25,78 @@ async function $import(uri, options) {
30
25
  catch (error) {
31
26
  if (error instanceof SyntaxError && uri.includes("jsdelivr")) {
32
27
  error.message += dedent `\n\n
33
- Are you sure that the file exists on JSDelivr?
28
+ Are you sure that the file exists on JSDelivr?
34
29
 
35
- The error indicates that the imported file does not exist on JSDelivr. For non-existent files, JSDelivr returns a 404 text that JS cannot parse as a module and throws a SyntaxError.
36
- `;
30
+ The error indicates that the imported file does not exist on JSDelivr. For non-existent files, JSDelivr returns a 404 text that JS cannot parse as a module and throws a SyntaxError.`;
37
31
  }
38
32
  throw new ModuleImportError({ module: uri, cause: error });
39
33
  }
40
34
  }
35
+ /**
36
+ * Tries to read the module from disk
37
+ * @throws {ModuleImportError}
38
+ */
39
+ async function readModulefromDisk(uri, readFile) {
40
+ try {
41
+ return await readFile(uri, { encoding: "utf-8" });
42
+ }
43
+ catch (error) {
44
+ throw new ModuleImportError({ module: uri, cause: error });
45
+ }
46
+ }
47
+ /**
48
+ * Reads a module from a CDN.
49
+ * Tries to read local cache first.
50
+ *
51
+ * @param uri A valid URL
52
+ * @throws {ModuleImportError}
53
+ */
54
+ async function readModuleFromCDN(uri) {
55
+ if (!isValidUrl(uri))
56
+ throw new ModuleImportError({ module: uri, cause: new Error("Malformed URL") });
57
+ const result = await tryCatch(async () => await fetch(uri));
58
+ if (result.error) {
59
+ throw new ModuleImportError({
60
+ module: uri,
61
+ cause: result.error,
62
+ });
63
+ }
64
+ const response = result.data;
65
+ if (!response.ok) {
66
+ throw new ModuleImportError({
67
+ module: uri,
68
+ cause: new Error(`Failed to fetch module. HTTP status: ${response.status}, Message: ${response.statusText}`),
69
+ });
70
+ }
71
+ const JS_CONTENT_TYPES = [
72
+ "application/javascript",
73
+ "text/javascript",
74
+ "application/x-javascript",
75
+ "text/x-javascript",
76
+ ];
77
+ // if there is no content-type header, assume it's a JavaScript module & hope for the best
78
+ const contentType = response.headers.get("content-type")?.toLowerCase();
79
+ if (contentType && !JS_CONTENT_TYPES.some((knownType) => contentType.includes(knownType))) {
80
+ throw new ModuleImportError({
81
+ module: uri,
82
+ cause: new Error(`Server responded with ${contentType} insetad of a JavaScript module`),
83
+ });
84
+ }
85
+ return await response.text();
86
+ }
87
+ function isValidUrl(url) {
88
+ // This dance is necessary to both support a fallback in case URL.canParse
89
+ // is not present (like in vitest), and also appease typescript
90
+ const URLConstructor = URL;
91
+ if ("canParse" in URL) {
92
+ return URL.canParse(url);
93
+ }
94
+ try {
95
+ new URLConstructor(url);
96
+ return true;
97
+ }
98
+ catch (e) {
99
+ console.warn(`Invalid URL: ${url}`);
100
+ return false;
101
+ }
102
+ }
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
4
4
  import { createImport } from "./import.js";
5
5
  describe("$import", async () => {
6
6
  const fs = createNodeishMemoryFs();
7
+ await fs.mkdir("./project.inlang/cache/modules", { recursive: true });
7
8
  await fs.writeFile("./mock-module.js", `
8
9
  export function hello() {
9
10
  return "hello";
@@ -16,7 +17,8 @@ describe("$import", async () => {
16
17
  return "world";
17
18
  }
18
19
  `);
19
- const _import = createImport({
20
+ const _import = createImport("/project.inlang", {
21
+ writeFile: fs.writeFile,
20
22
  readFile: fs.readFile,
21
23
  });
22
24
  it("should import a module from a local path", async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"resolveModules.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/resolveModules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAoBvD,eAAO,MAAM,cAAc,EAAE,qBAiG5B,CAAA"}
1
+ {"version":3,"file":"resolveModules.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/resolveModules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAoBvD,eAAO,MAAM,cAAc,EAAE,qBA4F5B,CAAA"}
@@ -8,30 +8,27 @@ import { TypeCompiler } from "@sinclair/typebox/compiler";
8
8
  import { validatedModuleSettings } from "./validatedModuleSettings.js";
9
9
  const ModuleCompiler = TypeCompiler.Compile(InlangModule);
10
10
  export const resolveModules = async (args) => {
11
- const _import = args._import ?? createImport({ readFile: args.nodeishFs.readFile });
12
- const moduleErrors = [];
11
+ const _import = args._import ?? createImport(args.projectPath, args.nodeishFs);
13
12
  const allPlugins = [];
14
13
  const allMessageLintRules = [];
15
14
  const meta = [];
16
- for (const module of args.settings.modules) {
17
- /**
18
- * -------------- BEGIN SETUP --------------
19
- */
15
+ const moduleErrors = [];
16
+ async function resolveModule(module) {
20
17
  const importedModule = await tryCatch(() => _import(module));
21
- // -- IMPORT MODULE --
18
+ // -- FAILED TO IMPORT --
22
19
  if (importedModule.error) {
23
20
  moduleErrors.push(new ModuleImportError({
24
21
  module: module,
25
22
  cause: importedModule.error,
26
23
  }));
27
- continue;
24
+ return;
28
25
  }
29
26
  // -- MODULE DOES NOT EXPORT ANYTHING --
30
27
  if (importedModule.data?.default === undefined) {
31
28
  moduleErrors.push(new ModuleHasNoExportsError({
32
29
  module: module,
33
30
  }));
34
- continue;
31
+ return;
35
32
  }
36
33
  // -- CHECK IF MODULE IS SYNTACTIALLY VALID
37
34
  const isValidModule = ModuleCompiler.Check(importedModule.data);
@@ -41,7 +38,7 @@ export const resolveModules = async (args) => {
41
38
  module: module,
42
39
  errors,
43
40
  }));
44
- continue;
41
+ return;
45
42
  }
46
43
  // -- VALIDATE MODULE SETTINGS
47
44
  const result = validatedModuleSettings({
@@ -50,7 +47,7 @@ export const resolveModules = async (args) => {
50
47
  });
51
48
  if (result !== "isValid") {
52
49
  moduleErrors.push(new ModuleSettingsAreInvalidError({ module: module, errors: result }));
53
- continue;
50
+ return;
54
51
  }
55
52
  meta.push({
56
53
  module: module,
@@ -66,6 +63,7 @@ export const resolveModules = async (args) => {
66
63
  moduleErrors.push(new ModuleError(`Unimplemented module type ${importedModule.data.default.id}.The module has not been installed.`, { module: module }));
67
64
  }
68
65
  }
66
+ await Promise.all(args.settings.modules.map(resolveModule));
69
67
  const resolvedPlugins = await resolvePlugins({
70
68
  plugins: allPlugins,
71
69
  settings: args.settings,
@@ -9,6 +9,7 @@ it("should return an error if a plugin cannot be imported", async () => {
9
9
  modules: ["https://myplugin.com/index.js"],
10
10
  };
11
11
  const resolved = await resolveModules({
12
+ projectPath: "/project.inlang",
12
13
  settings,
13
14
  nodeishFs: {},
14
15
  _import: () => {
@@ -58,7 +59,12 @@ it("should resolve plugins and message lint rules successfully", async () => {
58
59
  }
59
60
  };
60
61
  // Call the function
61
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
62
+ const resolved = await resolveModules({
63
+ projectPath: "/project.inlang",
64
+ settings,
65
+ _import,
66
+ nodeishFs: {},
67
+ });
62
68
  // Assert results
63
69
  expect(resolved.errors).toHaveLength(0);
64
70
  // Check for the meta data of the plugin
@@ -81,7 +87,12 @@ it("should return an error if a module cannot be imported", async () => {
81
87
  });
82
88
  };
83
89
  // Call the function
84
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
90
+ const resolved = await resolveModules({
91
+ projectPath: "/project.inlang",
92
+ settings,
93
+ _import,
94
+ nodeishFs: {},
95
+ });
85
96
  // Assert results
86
97
  expect(resolved.errors[0]).toBeInstanceOf(ModuleImportError);
87
98
  });
@@ -93,7 +104,12 @@ it("should return an error if a module does not export anything", async () => {
93
104
  };
94
105
  const _import = async () => ({});
95
106
  // Call the function
96
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
107
+ const resolved = await resolveModules({
108
+ projectPath: "/project.inlang",
109
+ settings,
110
+ _import,
111
+ nodeishFs: {},
112
+ });
97
113
  // Assert results
98
114
  expect(resolved.errors[0]).toBeInstanceOf(ModuleHasNoExportsError);
99
115
  });
@@ -110,7 +126,12 @@ it("should return an error if a module exports an invalid plugin or lint rule",
110
126
  description: { en: "Mock plugin description" },
111
127
  },
112
128
  });
113
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
129
+ const resolved = await resolveModules({
130
+ projectPath: "/project.inlang",
131
+ settings,
132
+ _import,
133
+ nodeishFs: {},
134
+ });
114
135
  expect(resolved.errors[0]).toBeInstanceOf(ModuleExportIsInvalidError);
115
136
  });
116
137
  it("should handle other unhandled errors during plugin resolution", async () => {
@@ -124,7 +145,12 @@ it("should handle other unhandled errors during plugin resolution", async () =>
124
145
  throw new Error(errorMessage);
125
146
  };
126
147
  // Call the function
127
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
148
+ const resolved = await resolveModules({
149
+ projectPath: "/project.inlang",
150
+ settings,
151
+ _import,
152
+ nodeishFs: {},
153
+ });
128
154
  // Assert results
129
155
  expect(resolved.errors[0]).toBeInstanceOf(ModuleError);
130
156
  });
@@ -148,7 +174,12 @@ it("should return an error if a moduleSettings are invalid", async () => {
148
174
  },
149
175
  });
150
176
  // Call the function
151
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} });
177
+ const resolved = await resolveModules({
178
+ projectPath: "/project.inlang",
179
+ settings,
180
+ _import,
181
+ nodeishFs: {},
182
+ });
152
183
  // Assert results
153
184
  expect(resolved.errors[0]).toBeInstanceOf(ModuleSettingsAreInvalidError);
154
185
  });
@@ -11,6 +11,7 @@ import type { resolveMessageLintRules } from "./message-lint-rules/resolveMessag
11
11
  * Pass a custom `_import` function to override the default import function.
12
12
  */
13
13
  export type ResolveModuleFunction = (args: {
14
+ projectPath: string;
14
15
  settings: ProjectSettings;
15
16
  nodeishFs: NodeishFilesystemSubset;
16
17
  _import?: ImportFunction;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EACX,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAA;AAE9F;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE;IAC1C,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,uBAAuB,CAAA;IAClC,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,KAAK,OAAO,CAAC;IACb;;;;;;;;OAQG;IACH,IAAI,EAAE,KAAK,CAAC;QACX;;;;WAIG;QACH,MAAM,EAAE,MAAM,CAAA;QACd;;WAEG;QACH,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;KACxC,CAAC,CAAA;IACF;;OAEG;IACH,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACtB;;OAEG;IACH,gBAAgB,EAAE,KAAK,CAAC,eAAe,CAAC,CAAA;IACxC;;OAEG;IACH,iBAAiB,EAAE,iBAAiB,CAAA;IACpC;;;;;;;;OAQG;IACH,MAAM,EAAE,KAAK,CACV,uBAAuB,GACvB,iBAAiB,GACjB,OAAO,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAC7D,OAAO,CAAC,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CACvE,CAAA;CACD,CAAC,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/resolve-modules/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,KAAK,EACX,uBAAuB,EACvB,sBAAsB,EACtB,iBAAiB,EACjB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,KAAK,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACjD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iDAAiD,CAAA;AAE9F;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,IAAI,EAAE;IAC1C,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,uBAAuB,CAAA;IAClC,OAAO,CAAC,EAAE,cAAc,CAAA;CACxB,KAAK,OAAO,CAAC;IACb;;;;;;;;OAQG;IACH,IAAI,EAAE,KAAK,CAAC;QACX;;;;WAIG;QACH,MAAM,EAAE,MAAM,CAAA;QACd;;WAEG;QACH,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA;KACxC,CAAC,CAAA;IACF;;OAEG;IACH,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAA;IACtB;;OAEG;IACH,gBAAgB,EAAE,KAAK,CAAC,eAAe,CAAC,CAAA;IACxC;;OAEG;IACH,iBAAiB,EAAE,iBAAiB,CAAA;IACpC;;;;;;;;OAQG;IACH,MAAM,EAAE,KAAK,CACV,uBAAuB,GACvB,iBAAiB,GACjB,OAAO,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAC7D,OAAO,CAAC,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CACvE,CAAA;CACD,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../../src/v2/mocks/plural/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,eAAO,MAAM,YAAY,EAAE,aA2I1B,CAAA"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../../src/v2/mocks/plural/bundle.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,eAAO,MAAM,YAAY,EAAE,aAiK1B,CAAA"}
@@ -18,6 +18,28 @@ export const pluralBundle = {
18
18
  },
19
19
  },
20
20
  },
21
+ {
22
+ type: "input",
23
+ name: "count",
24
+ value: {
25
+ type: "expression",
26
+ arg: {
27
+ type: "variable",
28
+ name: "count",
29
+ },
30
+ },
31
+ },
32
+ {
33
+ type: "input",
34
+ name: "projectCount",
35
+ value: {
36
+ type: "expression",
37
+ arg: {
38
+ type: "variable",
39
+ name: "projectCount",
40
+ },
41
+ },
42
+ },
21
43
  ],
22
44
  selectors: [
23
45
  {
@@ -8,7 +8,7 @@ describe("mock plural messageBundle", () => {
8
8
  const messageBundle = pluralBundle;
9
9
  expect(Value.Check(MessageBundle, messageBundle)).toBe(true);
10
10
  expect(pluralBundle.messages.length).toBe(2);
11
- expect(pluralBundle.messages[0].declarations.length).toBe(1);
11
+ expect(pluralBundle.messages[0].declarations.length).toBe(3);
12
12
  expect(pluralBundle.messages[0].selectors.length).toBe(1);
13
13
  expect(pluralBundle.messages[0].variants.length).toBe(3);
14
14
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/sdk",
3
3
  "type": "module",
4
- "version": "0.35.8",
4
+ "version": "0.36.0",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -34,17 +34,17 @@
34
34
  "murmurhash3js": "^3.0.1",
35
35
  "solid-js": "1.6.12",
36
36
  "throttle-debounce": "^5.0.0",
37
+ "@inlang/json-types": "1.1.0",
37
38
  "@inlang/language-tag": "1.5.1",
38
- "@inlang/message": "2.1.0",
39
- "@inlang/module": "1.2.13",
40
39
  "@inlang/message-lint-rule": "1.4.7",
41
- "@inlang/json-types": "1.1.0",
42
40
  "@inlang/plugin": "2.4.13",
41
+ "@inlang/message": "2.1.0",
42
+ "@inlang/module": "1.2.13",
43
43
  "@inlang/project-settings": "2.4.2",
44
44
  "@inlang/result": "1.1.0",
45
+ "@inlang/translatable": "1.3.1",
45
46
  "@lix-js/client": "2.2.0",
46
- "@lix-js/fs": "2.1.0",
47
- "@inlang/translatable": "1.3.1"
47
+ "@lix-js/fs": "2.1.0"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/debug": "^4.1.12",
@@ -80,7 +80,7 @@ describe("createNewProject", () => {
80
80
  await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
81
81
 
82
82
  const project = await loadProject({ projectPath, repo })
83
- expect(project.errors().length).toBe(0)
83
+ expect(project.errors()).toEqual([])
84
84
  })
85
85
 
86
86
  it("should create messages inside the project directory", async () => {
@@ -88,7 +88,7 @@ describe("createNewProject", () => {
88
88
  const projectPath = "/test/project.inlang"
89
89
  await createNewProject({ projectPath, repo, projectSettings: defaultProjectSettings })
90
90
  const project = await loadProject({ projectPath, repo })
91
- expect(project.errors().length).toBe(0)
91
+ expect(project.errors()).toEqual([])
92
92
 
93
93
  const testMessage = createMessage("test", { en: "test message" })
94
94
  project.query.messages.create({ data: testMessage })
@@ -18,9 +18,12 @@ export async function createNewProject(args: {
18
18
  if (await pathExists(args.projectPath, nodeishFs)) {
19
19
  throw new Error(`projectPath already exists, received "${args.projectPath}"`)
20
20
  }
21
- await nodeishFs.mkdir(args.projectPath, { recursive: true })
22
-
23
21
  const settingsText = JSON.stringify(args.projectSettings ?? defaultProjectSettings, undefined, 2)
24
22
 
25
- await nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText)
23
+ await nodeishFs.mkdir(args.projectPath, { recursive: true })
24
+ await Promise.all([
25
+ nodeishFs.writeFile(`${args.projectPath}/settings.json`, settingsText),
26
+ nodeishFs.writeFile(`${args.projectPath}/.gitignore`, "cache"),
27
+ nodeishFs.mkdir(`${args.projectPath}/cache/modules`, { recursive: true }),
28
+ ])
26
29
  }
package/src/index.ts CHANGED
@@ -25,7 +25,15 @@ export {
25
25
  PluginLoadMessagesError,
26
26
  PluginSaveMessagesError,
27
27
  } from "./errors.js"
28
+ export {
29
+ ModuleError,
30
+ ModuleHasNoExportsError,
31
+ ModuleImportError,
32
+ ModuleExportIsInvalidError,
33
+ ModuleSettingsAreInvalidError,
34
+ } from "./resolve-modules/errors.js"
28
35
 
36
+ export { randomHumanId } from "./storage/human-id/human-readable-id.js"
29
37
  export { normalizeMessage } from "./storage/helper.js"
30
38
  export * from "./messages/variant.js"
31
39
  export * from "./versionedInterfaces.js"
@@ -23,12 +23,14 @@ import { migrateIfOutdated } from "@inlang/project-settings/migration"
23
23
  import { createNodeishFsWithAbsolutePaths } from "./createNodeishFsWithAbsolutePaths.js"
24
24
  import { normalizePath } from "@lix-js/fs"
25
25
  import { assertValidProjectPath } from "./validateProjectPath.js"
26
+
27
+ // Migrations
26
28
  import { maybeMigrateToDirectory } from "./migrations/migrateToDirectory.js"
29
+ import { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectId.js"
30
+ import { maybeAddModuleCache } from "./migrations/maybeAddModuleCache.js"
27
31
 
28
32
  import type { Repository } from "@lix-js/client"
29
33
 
30
- import { maybeCreateFirstProjectId } from "./migrations/maybeCreateFirstProjectId.js"
31
-
32
34
  import { capture } from "./telemetry/capture.js"
33
35
  import { identifyProject } from "./telemetry/groupIdentify.js"
34
36
 
@@ -74,6 +76,7 @@ export async function loadProject(args: {
74
76
 
75
77
  await maybeMigrateToDirectory({ nodeishFs, projectPath })
76
78
  await maybeCreateFirstProjectId({ projectPath, repo: args.repo })
79
+ await maybeAddModuleCache({ projectPath, repo: args.repo })
77
80
 
78
81
  // -- load project ------------------------------------------------------
79
82
 
@@ -155,7 +158,12 @@ export async function loadProject(args: {
155
158
  const _settings = settings()
156
159
  if (!_settings) return
157
160
 
158
- resolveModules({ settings: _settings, nodeishFs, _import: args._import })
161
+ resolveModules({
162
+ settings: _settings,
163
+ nodeishFs,
164
+ _import: args._import,
165
+ projectPath,
166
+ })
159
167
  .then((resolvedModules) => {
160
168
  setResolvedModules(resolvedModules)
161
169
  })
@@ -0,0 +1,53 @@
1
+ import type { Repository } from "@lix-js/client"
2
+ import type { NodeishFilesystem } from "@lix-js/fs"
3
+
4
+ const EXPECTED_IGNORES = ["cache"]
5
+
6
+ export async function maybeAddModuleCache(args: {
7
+ projectPath: string
8
+ repo?: Repository
9
+ }): Promise<void> {
10
+ if (args.repo === undefined) return
11
+
12
+ const projectExists = await directoryExists(args.projectPath, args.repo.nodeishFs)
13
+ if (!projectExists) return
14
+
15
+ const gitignorePath = args.projectPath + "/.gitignore"
16
+ const moduleCache = args.projectPath + "/cache/modules/"
17
+
18
+ const gitignoreExists = await fileExists(gitignorePath, args.repo.nodeishFs)
19
+ const moduleCacheExists = await directoryExists(moduleCache, args.repo.nodeishFs)
20
+
21
+ if (gitignoreExists) {
22
+ // non-destructively add any missing ignores
23
+ const gitignore = await args.repo.nodeishFs.readFile(gitignorePath, { encoding: "utf-8" })
24
+ const missingIgnores = EXPECTED_IGNORES.filter((ignore) => !gitignore.includes(ignore))
25
+ if (missingIgnores.length > 0) {
26
+ await args.repo.nodeishFs.appendFile(gitignorePath, "\n" + missingIgnores.join("\n"))
27
+ }
28
+ } else {
29
+ await args.repo.nodeishFs.writeFile(gitignorePath, EXPECTED_IGNORES.join("\n"))
30
+ }
31
+
32
+ if (!moduleCacheExists) {
33
+ await args.repo.nodeishFs.mkdir(moduleCache, { recursive: true })
34
+ }
35
+ }
36
+
37
+ async function fileExists(path: string, nodeishFs: NodeishFilesystem): Promise<boolean> {
38
+ try {
39
+ const stat = await nodeishFs.stat(path)
40
+ return stat.isFile()
41
+ } catch {
42
+ return false
43
+ }
44
+ }
45
+
46
+ async function directoryExists(path: string, nodeishFs: NodeishFilesystem): Promise<boolean> {
47
+ try {
48
+ const stat = await nodeishFs.stat(path)
49
+ return stat.isDirectory()
50
+ } catch {
51
+ return false
52
+ }
53
+ }
@@ -0,0 +1,62 @@
1
+ import type { NodeishFilesystemSubset } from "@inlang/plugin"
2
+ import { type Result, tryCatch } from "@inlang/result"
3
+
4
+ function escape(url: string) {
5
+ // collect the bytes of the UTF-8 representation
6
+ const bytes = new TextEncoder().encode(url)
7
+
8
+ // 64-bit FNV1a hash to make the file-names shorter
9
+ // https://en.wikipedia.org/wiki/FNV-1a
10
+ const hash = bytes.reduce(
11
+ (hash, byte) => BigInt.asUintN(64, (hash ^ BigInt(byte)) * 1_099_511_628_211n),
12
+ 14_695_981_039_346_656_037n
13
+ )
14
+
15
+ return hash.toString(36)
16
+ }
17
+
18
+ async function readModuleFromCache(
19
+ moduleURI: string,
20
+ projectPath: string,
21
+ readFile: NodeishFilesystemSubset["readFile"]
22
+ ): Promise<Result<string, Error>> {
23
+ const moduleHash = escape(moduleURI)
24
+ const filePath = projectPath + `/cache/modules/${moduleHash}`
25
+
26
+ return await tryCatch(async () => await readFile(filePath, { encoding: "utf-8" }))
27
+ }
28
+
29
+ async function writeModuleToCache(
30
+ moduleURI: string,
31
+ moduleContent: string,
32
+ projectPath: string,
33
+ writeFile: NodeishFilesystemSubset["writeFile"]
34
+ ): Promise<void> {
35
+ const moduleHash = escape(moduleURI)
36
+ const filePath = projectPath + `/cache/modules/${moduleHash}`
37
+ await writeFile(filePath, moduleContent)
38
+ }
39
+
40
+ /**
41
+ * Implements a "Network-First" caching strategy.
42
+ */
43
+ export function withCache(
44
+ moduleLoader: (uri: string) => Promise<string>,
45
+ projectPath: string,
46
+ nodeishFs: Pick<NodeishFilesystemSubset, "readFile" | "writeFile">
47
+ ): (uri: string) => Promise<string> {
48
+ return async (uri: string) => {
49
+ const cachePromise = readModuleFromCache(uri, projectPath, nodeishFs.readFile)
50
+ const networkResult = await tryCatch(async () => await moduleLoader(uri))
51
+
52
+ if (networkResult.error) {
53
+ const cacheResult = await cachePromise
54
+ if (!cacheResult.error) return cacheResult.data
55
+ else throw networkResult.error
56
+ } else {
57
+ const moduleAsText = networkResult.data
58
+ await writeModuleToCache(uri, moduleAsText, projectPath, nodeishFs.writeFile)
59
+ return moduleAsText
60
+ }
61
+ }
62
+ }
@@ -6,6 +6,9 @@ import { createImport } from "./import.js"
6
6
 
7
7
  describe("$import", async () => {
8
8
  const fs = createNodeishMemoryFs()
9
+
10
+ await fs.mkdir("./project.inlang/cache/modules", { recursive: true })
11
+
9
12
  await fs.writeFile(
10
13
  "./mock-module.js",
11
14
  `
@@ -26,7 +29,8 @@ describe("$import", async () => {
26
29
  `
27
30
  )
28
31
 
29
- const _import = createImport({
32
+ const _import = createImport("/project.inlang", {
33
+ writeFile: fs.writeFile,
30
34
  readFile: fs.readFile,
31
35
  })
32
36
 
@@ -1,6 +1,8 @@
1
1
  import dedent from "dedent"
2
2
  import type { NodeishFilesystemSubset } from "@inlang/plugin"
3
3
  import { ModuleImportError } from "./errors.js"
4
+ import { withCache } from "./cache.js"
5
+ import { tryCatch } from "@inlang/result"
4
6
 
5
7
  /**
6
8
  * Importing ES modules either from a local path, or from a url.
@@ -19,32 +21,21 @@ export type ImportFunction = (uri: string) => Promise<any>
19
21
  * const $import = createImport({ readFile: fs.readFile, fetch });
20
22
  * const module = await _import('./some-module.js');
21
23
  */
22
- export function createImport(args: {
23
- /** the fs from which the file can be read */
24
- readFile: NodeishFilesystemSubset["readFile"]
25
- }): (uri: string) => ReturnType<typeof $import> {
26
- // resembles a native import api
27
- return (uri: string) => $import(uri, args)
24
+ export function createImport(
25
+ projectPath: string,
26
+ nodeishFs: Pick<NodeishFilesystemSubset, "readFile" | "writeFile">
27
+ ) {
28
+ return (uri: string) => $import(uri, projectPath, nodeishFs)
28
29
  }
29
30
 
30
31
  async function $import(
31
32
  uri: string,
32
- options: {
33
- /**
34
- * Required to import from a local path.
35
- */
36
- readFile: NodeishFilesystemSubset["readFile"]
37
- }
33
+ projectPath: string,
34
+ nodeishFs: Pick<NodeishFilesystemSubset, "readFile" | "writeFile">
38
35
  ): Promise<any> {
39
- let moduleAsText: string
40
-
41
- if (uri.startsWith("http")) {
42
- moduleAsText = await (await fetch(uri)).text()
43
- } else {
44
- moduleAsText = await options.readFile(uri, {
45
- encoding: "utf-8",
46
- })
47
- }
36
+ const moduleAsText = uri.startsWith("http")
37
+ ? await withCache(readModuleFromCDN, projectPath, nodeishFs)(uri)
38
+ : await readModulefromDisk(uri, nodeishFs.readFile)
48
39
 
49
40
  const moduleWithMimeType = "data:application/javascript," + encodeURIComponent(moduleAsText)
50
41
 
@@ -53,11 +44,90 @@ async function $import(
53
44
  } catch (error) {
54
45
  if (error instanceof SyntaxError && uri.includes("jsdelivr")) {
55
46
  error.message += dedent`\n\n
56
- Are you sure that the file exists on JSDelivr?
47
+ Are you sure that the file exists on JSDelivr?
57
48
 
58
- The error indicates that the imported file does not exist on JSDelivr. For non-existent files, JSDelivr returns a 404 text that JS cannot parse as a module and throws a SyntaxError.
59
- `
49
+ The error indicates that the imported file does not exist on JSDelivr. For non-existent files, JSDelivr returns a 404 text that JS cannot parse as a module and throws a SyntaxError.`
60
50
  }
61
51
  throw new ModuleImportError({ module: uri, cause: error as Error })
62
52
  }
63
53
  }
54
+
55
+ /**
56
+ * Tries to read the module from disk
57
+ * @throws {ModuleImportError}
58
+ */
59
+ async function readModulefromDisk(
60
+ uri: string,
61
+ readFile: NodeishFilesystemSubset["readFile"]
62
+ ): Promise<string> {
63
+ try {
64
+ return await readFile(uri, { encoding: "utf-8" })
65
+ } catch (error) {
66
+ throw new ModuleImportError({ module: uri, cause: error as Error })
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Reads a module from a CDN.
72
+ * Tries to read local cache first.
73
+ *
74
+ * @param uri A valid URL
75
+ * @throws {ModuleImportError}
76
+ */
77
+ async function readModuleFromCDN(uri: string): Promise<string> {
78
+ if (!isValidUrl(uri))
79
+ throw new ModuleImportError({ module: uri, cause: new Error("Malformed URL") })
80
+
81
+ const result = await tryCatch(async () => await fetch(uri))
82
+ if (result.error) {
83
+ throw new ModuleImportError({
84
+ module: uri,
85
+ cause: result.error,
86
+ })
87
+ }
88
+
89
+ const response = result.data
90
+ if (!response.ok) {
91
+ throw new ModuleImportError({
92
+ module: uri,
93
+ cause: new Error(
94
+ `Failed to fetch module. HTTP status: ${response.status}, Message: ${response.statusText}`
95
+ ),
96
+ })
97
+ }
98
+
99
+ const JS_CONTENT_TYPES = [
100
+ "application/javascript",
101
+ "text/javascript",
102
+ "application/x-javascript",
103
+ "text/x-javascript",
104
+ ]
105
+
106
+ // if there is no content-type header, assume it's a JavaScript module & hope for the best
107
+ const contentType = response.headers.get("content-type")?.toLowerCase()
108
+ if (contentType && !JS_CONTENT_TYPES.some((knownType) => contentType.includes(knownType))) {
109
+ throw new ModuleImportError({
110
+ module: uri,
111
+ cause: new Error(`Server responded with ${contentType} insetad of a JavaScript module`),
112
+ })
113
+ }
114
+
115
+ return await response.text()
116
+ }
117
+
118
+ function isValidUrl(url: string) {
119
+ // This dance is necessary to both support a fallback in case URL.canParse
120
+ // is not present (like in vitest), and also appease typescript
121
+ const URLConstructor = URL
122
+ if ("canParse" in URL) {
123
+ return URL.canParse(url)
124
+ }
125
+
126
+ try {
127
+ new URLConstructor(url)
128
+ return true
129
+ } catch (e) {
130
+ console.warn(`Invalid URL: ${url}`)
131
+ return false
132
+ }
133
+ }
@@ -22,6 +22,7 @@ it("should return an error if a plugin cannot be imported", async () => {
22
22
  }
23
23
 
24
24
  const resolved = await resolveModules({
25
+ projectPath: "/project.inlang",
25
26
  settings,
26
27
  nodeishFs: {} as any,
27
28
  _import: () => {
@@ -78,7 +79,12 @@ it("should resolve plugins and message lint rules successfully", async () => {
78
79
  }
79
80
 
80
81
  // Call the function
81
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
82
+ const resolved = await resolveModules({
83
+ projectPath: "/project.inlang",
84
+ settings,
85
+ _import,
86
+ nodeishFs: {} as any,
87
+ })
82
88
 
83
89
  // Assert results
84
90
  expect(resolved.errors).toHaveLength(0)
@@ -105,7 +111,12 @@ it("should return an error if a module cannot be imported", async () => {
105
111
  }
106
112
 
107
113
  // Call the function
108
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
114
+ const resolved = await resolveModules({
115
+ projectPath: "/project.inlang",
116
+ settings,
117
+ _import,
118
+ nodeishFs: {} as any,
119
+ })
109
120
 
110
121
  // Assert results
111
122
  expect(resolved.errors[0]).toBeInstanceOf(ModuleImportError)
@@ -121,7 +132,12 @@ it("should return an error if a module does not export anything", async () => {
121
132
  const _import = async () => ({})
122
133
 
123
134
  // Call the function
124
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
135
+ const resolved = await resolveModules({
136
+ projectPath: "/project.inlang",
137
+ settings,
138
+ _import,
139
+ nodeishFs: {} as any,
140
+ })
125
141
 
126
142
  // Assert results
127
143
  expect(resolved.errors[0]).toBeInstanceOf(ModuleHasNoExportsError)
@@ -142,7 +158,12 @@ it("should return an error if a module exports an invalid plugin or lint rule",
142
158
  },
143
159
  } satisfies InlangModule)
144
160
 
145
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
161
+ const resolved = await resolveModules({
162
+ projectPath: "/project.inlang",
163
+ settings,
164
+ _import,
165
+ nodeishFs: {} as any,
166
+ })
146
167
  expect(resolved.errors[0]).toBeInstanceOf(ModuleExportIsInvalidError)
147
168
  })
148
169
 
@@ -159,7 +180,12 @@ it("should handle other unhandled errors during plugin resolution", async () =>
159
180
  }
160
181
 
161
182
  // Call the function
162
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
183
+ const resolved = await resolveModules({
184
+ projectPath: "/project.inlang",
185
+ settings,
186
+ _import,
187
+ nodeishFs: {} as any,
188
+ })
163
189
 
164
190
  // Assert results
165
191
  expect(resolved.errors[0]).toBeInstanceOf(ModuleError)
@@ -186,7 +212,12 @@ it("should return an error if a moduleSettings are invalid", async () => {
186
212
  })
187
213
 
188
214
  // Call the function
189
- const resolved = await resolveModules({ settings, _import, nodeishFs: {} as any })
215
+ const resolved = await resolveModules({
216
+ projectPath: "/project.inlang",
217
+ settings,
218
+ _import,
219
+ nodeishFs: {} as any,
220
+ })
190
221
 
191
222
  // Assert results
192
223
  expect(resolved.errors[0]).toBeInstanceOf(ModuleSettingsAreInvalidError)
@@ -9,32 +9,27 @@ import {
9
9
  } from "./errors.js"
10
10
  import { tryCatch } from "@inlang/result"
11
11
  import { resolveMessageLintRules } from "./message-lint-rules/resolveMessageLintRules.js"
12
- import type { Plugin } from "@inlang/plugin"
13
12
  import { createImport } from "./import.js"
14
- import type { MessageLintRule } from "@inlang/message-lint-rule"
15
13
  import { resolvePlugins } from "./plugins/resolvePlugins.js"
16
14
  import { TypeCompiler } from "@sinclair/typebox/compiler"
17
15
  import { validatedModuleSettings } from "./validatedModuleSettings.js"
16
+ import type { Plugin } from "@inlang/plugin"
17
+ import type { MessageLintRule } from "@inlang/message-lint-rule"
18
18
 
19
19
  const ModuleCompiler = TypeCompiler.Compile(InlangModule)
20
20
 
21
21
  export const resolveModules: ResolveModuleFunction = async (args) => {
22
- const _import = args._import ?? createImport({ readFile: args.nodeishFs.readFile })
23
- const moduleErrors: Array<ModuleError> = []
22
+ const _import = args._import ?? createImport(args.projectPath, args.nodeishFs)
24
23
 
25
24
  const allPlugins: Array<Plugin> = []
26
25
  const allMessageLintRules: Array<MessageLintRule> = []
27
-
28
26
  const meta: Awaited<ReturnType<ResolveModuleFunction>>["meta"] = []
27
+ const moduleErrors: Array<ModuleError> = []
29
28
 
30
- for (const module of args.settings.modules) {
31
- /**
32
- * -------------- BEGIN SETUP --------------
33
- */
34
-
29
+ async function resolveModule(module: string) {
35
30
  const importedModule = await tryCatch<InlangModule>(() => _import(module))
36
- // -- IMPORT MODULE --
37
31
 
32
+ // -- FAILED TO IMPORT --
38
33
  if (importedModule.error) {
39
34
  moduleErrors.push(
40
35
  new ModuleImportError({
@@ -42,22 +37,20 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
42
37
  cause: importedModule.error as Error,
43
38
  })
44
39
  )
45
- continue
40
+ return
46
41
  }
47
42
 
48
43
  // -- MODULE DOES NOT EXPORT ANYTHING --
49
-
50
44
  if (importedModule.data?.default === undefined) {
51
45
  moduleErrors.push(
52
46
  new ModuleHasNoExportsError({
53
47
  module: module,
54
48
  })
55
49
  )
56
- continue
50
+ return
57
51
  }
58
52
 
59
53
  // -- CHECK IF MODULE IS SYNTACTIALLY VALID
60
-
61
54
  const isValidModule = ModuleCompiler.Check(importedModule.data)
62
55
  if (isValidModule === false) {
63
56
  const errors = [...ModuleCompiler.Errors(importedModule.data)]
@@ -68,7 +61,7 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
68
61
  })
69
62
  )
70
63
 
71
- continue
64
+ return
72
65
  }
73
66
 
74
67
  // -- VALIDATE MODULE SETTINGS
@@ -79,7 +72,7 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
79
72
  })
80
73
  if (result !== "isValid") {
81
74
  moduleErrors.push(new ModuleSettingsAreInvalidError({ module: module, errors: result }))
82
- continue
75
+ return
83
76
  }
84
77
 
85
78
  meta.push({
@@ -100,6 +93,8 @@ export const resolveModules: ResolveModuleFunction = async (args) => {
100
93
  )
101
94
  }
102
95
  }
96
+
97
+ await Promise.all(args.settings.modules.map(resolveModule))
103
98
  const resolvedPlugins = await resolvePlugins({
104
99
  plugins: allPlugins,
105
100
  settings: args.settings,
@@ -16,6 +16,7 @@ import type { resolveMessageLintRules } from "./message-lint-rules/resolveMessag
16
16
  * Pass a custom `_import` function to override the default import function.
17
17
  */
18
18
  export type ResolveModuleFunction = (args: {
19
+ projectPath: string
19
20
  settings: ProjectSettings
20
21
  nodeishFs: NodeishFilesystemSubset
21
22
  _import?: ImportFunction
@@ -11,7 +11,7 @@ describe("mock plural messageBundle", () => {
11
11
  expect(Value.Check(MessageBundle, messageBundle)).toBe(true)
12
12
 
13
13
  expect(pluralBundle.messages.length).toBe(2)
14
- expect(pluralBundle.messages[0]!.declarations.length).toBe(1)
14
+ expect(pluralBundle.messages[0]!.declarations.length).toBe(3)
15
15
  expect(pluralBundle.messages[0]!.selectors.length).toBe(1)
16
16
  expect(pluralBundle.messages[0]!.variants.length).toBe(3)
17
17
  })
@@ -20,6 +20,28 @@ export const pluralBundle: MessageBundle = {
20
20
  },
21
21
  },
22
22
  },
23
+ {
24
+ type: "input",
25
+ name: "count",
26
+ value: {
27
+ type: "expression",
28
+ arg: {
29
+ type: "variable",
30
+ name: "count",
31
+ },
32
+ },
33
+ },
34
+ {
35
+ type: "input",
36
+ name: "projectCount",
37
+ value: {
38
+ type: "expression",
39
+ arg: {
40
+ type: "variable",
41
+ name: "projectCount",
42
+ },
43
+ },
44
+ },
23
45
  ],
24
46
  selectors: [
25
47
  {