diff --git a/.gitignore b/.gitignore index 2ebd082b17d..f80e9cb45af 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,7 @@ src/go-landing **/dist **/dist/ +**/dist-esm/ **/dist-measure/ **/dist-prod/ **/dist-prod-measure/ diff --git a/src/packages/api-client/package.json b/src/packages/api-client/package.json index 71791c9d163..e08c6a1d3e0 100644 --- a/src/packages/api-client/package.json +++ b/src/packages/api-client/package.json @@ -3,12 +3,30 @@ "version": "0.1.2", "description": "CoCalc api client - use cocalc from nodejs", "main": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist-esm/index.js", + "default": "./dist/index.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./src/*": { + "types": "./dist/src/*.d.ts", + "import": "./dist-esm/src/*.js", + "default": "./dist/src/*.js" + } + }, + "files": ["dist/**", "dist-esm/**", "bin/**", "README.md", "package.json"], "scripts": { "preinstall": "npx only-allow pnpm", - "build": "../node_modules/.bin/tsc --build", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", + "clean": "rm -rf node_modules dist dist-esm", "depcheck": "pnpx depcheck --ignores @cocalc/api-client " }, - "files": ["dist/**", "bin/**", "README.md", "package.json"], "author": "SageMath, Inc.", "keywords": ["cocalc", "jupyter"], "license": "SEE LICENSE.md", diff --git a/src/packages/api-client/tsconfig-esm.json b/src/packages/api-client/tsconfig-esm.json new file mode 100644 index 00000000000..a78c78ec542 --- /dev/null +++ b/src/packages/api-client/tsconfig-esm.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/api-client/tsconfig.json b/src/packages/api-client/tsconfig.json index cacab0cc85c..6591776d9cc 100644 --- a/src/packages/api-client/tsconfig.json +++ b/src/packages/api-client/tsconfig.json @@ -4,6 +4,6 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references": [{ "path": "../backend" }] } diff --git a/src/packages/backend/package.json b/src/packages/backend/package.json index 7b3addadf33..a28f3975e70 100644 --- a/src/packages/backend/package.json +++ b/src/packages/backend/package.json @@ -3,12 +3,36 @@ "version": "1.22.2", "description": "CoCalc backend functionality: functionality used by either the hub, the next.js server or the project.", "exports": { - "./*": "./dist/*.js", - "./database": "./dist/database/index.js", - "./conat": "./dist/conat/index.js", - "./server-settings": "./dist/server-settings/index.js", - "./auth/*": "./dist/auth/*.js", - "./auth/tokens/*": "./dist/auth/tokens/*.js" + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./database": { + "types": "./dist/database/index.d.ts", + "import": "./dist-esm/database/index.js", + "default": "./dist/database/index.js" + }, + "./conat": { + "types": "./dist/conat/index.d.ts", + "import": "./dist-esm/conat/index.js", + "default": "./dist/conat/index.js" + }, + "./server-settings": { + "types": "./dist/server-settings/index.d.ts", + "import": "./dist-esm/server-settings/index.js", + "default": "./dist/server-settings/index.js" + }, + "./auth/*": { + "types": "./dist/auth/*.d.ts", + "import": "./dist-esm/auth/*.js", + "default": "./dist/auth/*.js" + }, + "./auth/tokens/*": { + "types": "./dist/auth/tokens/*.d.ts", + "import": "./dist-esm/auth/tokens/*.js", + "default": "./dist/auth/tokens/*.js" + } }, "keywords": [ "utilities", @@ -16,8 +40,8 @@ ], "scripts": { "preinstall": "npx only-allow pnpm", - "clean": "rm -rf dist node_modules", - "build": "pnpm exec tsc --build", + "clean": "rm -rf dist dist-esm node_modules", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", "test": "pnpm exec jest --forceExit", "test-conat": " pnpm exec jest --forceExit conat", "testp": "pnpm exec jest --forceExit", @@ -32,6 +56,7 @@ }, "files": [ "dist/**", + "dist-esm/**", "bin/**", "README.md", "package.json" diff --git a/src/packages/backend/tsconfig-esm.json b/src/packages/backend/tsconfig-esm.json new file mode 100644 index 00000000000..3f30680a9a1 --- /dev/null +++ b/src/packages/backend/tsconfig-esm.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true, + "paths": { + "@cocalc/backend/*": ["./*"] + } + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/backend/tsconfig.json b/src/packages/backend/tsconfig.json index 148fecfd626..288674f56d4 100644 --- a/src/packages/backend/tsconfig.json +++ b/src/packages/backend/tsconfig.json @@ -6,6 +6,6 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references": [{ "path": "../util", "path": "../conat" }] } diff --git a/src/packages/comm/package.json b/src/packages/comm/package.json index 0d2f9e6ef32..5e746cca097 100644 --- a/src/packages/comm/package.json +++ b/src/packages/comm/package.json @@ -3,20 +3,38 @@ "version": "0.1.0", "description": "Communication between project and frontend app support", "exports": { - "./*": "./dist/*.js", - "./websocket/*": "./dist/websocket/*.js", - "./project-status/*": "./dist/project-status/*.js", - "./project-info/*": "./dist/project-info/*.js" + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./websocket/*": { + "types": "./dist/websocket/*.d.ts", + "import": "./dist-esm/websocket/*.js", + "default": "./dist/websocket/*.js" + }, + "./project-status/*": { + "types": "./dist/project-status/*.d.ts", + "import": "./dist-esm/project-status/*.js", + "default": "./dist/project-status/*.js" + }, + "./project-info/*": { + "types": "./dist/project-info/*.d.ts", + "import": "./dist-esm/project-info/*.js", + "default": "./dist/project-info/*.js" + } }, "files": [ "dist/**", + "dist-esm/**", "README.md", "package.json", "tsconfig.json" ], "scripts": { "preinstall": "npx only-allow pnpm", - "build": "../node_modules/.bin/tsc --build", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", + "clean": "rm -rf node_modules dist dist-esm", "depcheck": "pnpx depcheck --ignores @types/node" }, "author": "SageMath, Inc.", diff --git a/src/packages/comm/tsconfig-esm.json b/src/packages/comm/tsconfig-esm.json new file mode 100644 index 00000000000..a78c78ec542 --- /dev/null +++ b/src/packages/comm/tsconfig-esm.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/comm/tsconfig.json b/src/packages/comm/tsconfig.json index 33d0246c7a0..d76904054d8 100644 --- a/src/packages/comm/tsconfig.json +++ b/src/packages/comm/tsconfig.json @@ -4,7 +4,7 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references": [ { "path": "../sync" }, { "path": "../jupyter" }, diff --git a/src/packages/conat/package.json b/src/packages/conat/package.json index 60baba51b37..9ed2ad4c8e1 100644 --- a/src/packages/conat/package.json +++ b/src/packages/conat/package.json @@ -3,28 +3,77 @@ "version": "1.0.0", "description": "Conat -- pub/sub framework. Usable by both nodejs and browser.", "exports": { - "./sync/*": "./dist/sync/*.js", - "./llm/*": "./dist/llm/*.js", - "./socket": "./dist/socket/index.js", - "./socket/*": "./dist/socket/*.js", - "./hub/changefeeds": "./dist/hub/changefeeds/index.js", - "./hub/api": "./dist/hub/api/index.js", - "./hub/api/*": "./dist/hub/api/*.js", - "./compute/*": "./dist/compute/*.js", - "./service": "./dist/service/index.js", - "./project/api": "./dist/project/api/index.js", - "./browser-api": "./dist/browser-api/index.js", - "./*": "./dist/*.js" + "./sync/*": { + "types": "./dist/sync/*.d.ts", + "import": "./dist-esm/sync/*.js", + "default": "./dist/sync/*.js" + }, + "./llm/*": { + "types": "./dist/llm/*.d.ts", + "import": "./dist-esm/llm/*.js", + "default": "./dist/llm/*.js" + }, + "./socket": { + "types": "./dist/socket/index.d.ts", + "import": "./dist-esm/socket/index.js", + "default": "./dist/socket/index.js" + }, + "./socket/*": { + "types": "./dist/socket/*.d.ts", + "import": "./dist-esm/socket/*.js", + "default": "./dist/socket/*.js" + }, + "./hub/changefeeds": { + "types": "./dist/hub/changefeeds/index.d.ts", + "import": "./dist-esm/hub/changefeeds/index.js", + "default": "./dist/hub/changefeeds/index.js" + }, + "./hub/api": { + "types": "./dist/hub/api/index.d.ts", + "import": "./dist-esm/hub/api/index.js", + "default": "./dist/hub/api/index.js" + }, + "./hub/api/*": { + "types": "./dist/hub/api/*.d.ts", + "import": "./dist-esm/hub/api/*.js", + "default": "./dist/hub/api/*.js" + }, + "./compute/*": { + "types": "./dist/compute/*.d.ts", + "import": "./dist-esm/compute/*.js", + "default": "./dist/compute/*.js" + }, + "./service": { + "types": "./dist/service/index.d.ts", + "import": "./dist-esm/service/index.js", + "default": "./dist/service/index.js" + }, + "./project/api": { + "types": "./dist/project/api/index.d.ts", + "import": "./dist-esm/project/api/index.js", + "default": "./dist/project/api/index.js" + }, + "./browser-api": { + "types": "./dist/browser-api/index.d.ts", + "import": "./dist-esm/browser-api/index.js", + "default": "./dist/browser-api/index.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + } }, "scripts": { "preinstall": "npx only-allow pnpm", - "build": "pnpm exec tsc --build", - "clean": "rm -rf dist node_modules", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", + "clean": "rm -rf dist dist-esm node_modules", "test": "pnpm exec jest", "depcheck": "pnpx depcheck --ignores events,bufferutil,utf-8-validate" }, "files": [ "dist/**", + "dist-esm/**", "README.md", "package.json" ], diff --git a/src/packages/conat/tsconfig-esm.json b/src/packages/conat/tsconfig-esm.json new file mode 100644 index 00000000000..3d3cafccc3b --- /dev/null +++ b/src/packages/conat/tsconfig-esm.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true, + "paths": { + "@cocalc/conat/*": ["./*"] + } + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/conat/tsconfig.json b/src/packages/conat/tsconfig.json index 687201523d0..3df517e77b4 100644 --- a/src/packages/conat/tsconfig.json +++ b/src/packages/conat/tsconfig.json @@ -4,7 +4,7 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references_comment": "Do not define path:../comm because that causes a circular references.", "references": [{ "path": "../util" }] } diff --git a/src/packages/pnpm-lock.yaml b/src/packages/pnpm-lock.yaml index 3f75997cf66..fa3ef7f9e8a 100644 --- a/src/packages/pnpm-lock.yaml +++ b/src/packages/pnpm-lock.yaml @@ -75,6 +75,8 @@ importers: specifier: ^18.16.14 version: 18.19.122 + api-client/dist-esm: {} + assets: dependencies: jquery: @@ -152,6 +154,8 @@ importers: specifier: ^18.16.14 version: 18.19.122 + backend/dist-esm: {} + cdn: devDependencies: codemirror: @@ -177,6 +181,8 @@ importers: specifier: ^18.16.14 version: 18.19.122 + comm/dist-esm: {} + conat: dependencies: '@cocalc/comm': @@ -244,6 +250,8 @@ importers: specifier: ^18.16.14 version: 18.19.122 + conat/dist-esm: {} + database: dependencies: '@cocalc/backend': @@ -1764,6 +1772,8 @@ importers: specifier: ^7.3.9 version: 7.3.9 + sync-client/dist-esm: {} + sync-fs: dependencies: '@cocalc/api-client': @@ -1798,6 +1808,10 @@ importers: specifier: ^18.16.14 version: 18.19.122 + sync-fs/dist-esm: {} + + sync/dist-esm: {} + util: dependencies: '@ant-design/colors': @@ -1866,9 +1880,6 @@ importers: sha1: specifier: ^1.1.1 version: 1.1.1 - underscore: - specifier: ^1.12.1 - version: 1.13.7 utility-types: specifier: ^3.10.0 version: 3.11.0 @@ -1904,6 +1915,8 @@ importers: specifier: ^0.22.0 version: 0.22.0 + util/dist-esm: {} + packages: '@adobe/css-tools@4.4.3': diff --git a/src/packages/project/servers/hub/handle-message.ts b/src/packages/project/servers/hub/handle-message.ts index f9c13c23d7f..07dfae7c3cf 100644 --- a/src/packages/project/servers/hub/handle-message.ts +++ b/src/packages/project/servers/hub/handle-message.ts @@ -153,7 +153,7 @@ export default async function handleMessage( } if (mesg.id != null) { // send back confirmation that a signal was sent - socket.write_mesg("json", message.signal_sent({ id: mesg.id })); + socket.write_mesg("json", message.success({ id: mesg.id })); } return; diff --git a/src/packages/static/src/module-rules.ts b/src/packages/static/src/module-rules.ts index 2231b3c7c72..b18696f8ccd 100644 --- a/src/packages/static/src/module-rules.ts +++ b/src/packages/static/src/module-rules.ts @@ -18,6 +18,14 @@ export default function moduleRules( ): Configuration["module"] { return { rules: [ + { + // TypeScript emits ESM without .js extensions in import specifiers, + // but dist-esm/package.json declares "type":"module" which makes + // rspack enforce fully-specified imports. Disable that requirement. + test: /\.js$/, + include: /dist-esm/, + resolve: { fullySpecified: false }, + }, { test: /\.(js|jsx|ts|tsx|mjs|cjs)$/, // the swc-loader absolutely mangles some upstream libraries so they diff --git a/src/packages/sync-client/package.json b/src/packages/sync-client/package.json index b7352febacd..913db8c286b 100644 --- a/src/packages/sync-client/package.json +++ b/src/packages/sync-client/package.json @@ -4,19 +4,40 @@ "description": "CoCalc Lightweight Nodejs Sync Client", "main": "./dist/lib/index.js", "exports": { - ".": "./dist/lib/index.js", - "./*": "./dist/*.js", - "./lib/*": "./dist/lib/*.js" + ".": { + "types": "./dist/lib/index.d.ts", + "import": "./dist-esm/lib/index.js", + "default": "./dist/lib/index.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./lib/*": { + "types": "./dist/lib/*.d.ts", + "import": "./dist-esm/lib/*.js", + "default": "./dist/lib/*.js" + } }, - "files": ["dist/**", "bin/**", "README.md", "package.json"], + "files": [ + "dist/**", + "dist-esm/**", + "bin/**", + "README.md", + "package.json" + ], "scripts": { "preinstall": "npx only-allow pnpm", - "build": "../node_modules/.bin/tsc --build", - "clean": "rm -rf node_modules dist", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", + "clean": "rm -rf node_modules dist dist-esm", "depcheck": "pnpx depcheck" }, "author": "SageMath, Inc.", - "keywords": ["cocalc", "jupyter"], + "keywords": [ + "cocalc", + "jupyter" + ], "license": "SEE LICENSE.md", "dependencies": { "@cocalc/api-client": "workspace:*", diff --git a/src/packages/sync-client/tsconfig-esm.json b/src/packages/sync-client/tsconfig-esm.json new file mode 100644 index 00000000000..a78c78ec542 --- /dev/null +++ b/src/packages/sync-client/tsconfig-esm.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/sync-client/tsconfig.json b/src/packages/sync-client/tsconfig.json index 25705765cef..7c8ba4e1eaa 100644 --- a/src/packages/sync-client/tsconfig.json +++ b/src/packages/sync-client/tsconfig.json @@ -5,7 +5,7 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references": [ { "path": "../sync" }, { "path": "../api-client" }, diff --git a/src/packages/sync-fs/package.json b/src/packages/sync-fs/package.json index 909eccaf63a..ee28321dd79 100644 --- a/src/packages/sync-fs/package.json +++ b/src/packages/sync-fs/package.json @@ -4,20 +4,33 @@ "description": "CoCalc Filesystem Sync", "main": "./dist/lib/index.js", "exports": { - ".": "./dist/lib/index.js", - "./*": "./dist/*.js", - "./lib/*": "./dist/lib/*.js" + ".": { + "types": "./dist/lib/index.d.ts", + "import": "./dist-esm/lib/index.js", + "default": "./dist/lib/index.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./lib/*": { + "types": "./dist/lib/*.d.ts", + "import": "./dist-esm/lib/*.js", + "default": "./dist/lib/*.js" + } }, "files": [ "dist/**", + "dist-esm/**", "bin/**", "README.md", "package.json" ], "scripts": { "preinstall": "npx only-allow pnpm", - "build": "../node_modules/.bin/tsc --build", - "clean": "rm -rf node_modules dist", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", + "clean": "rm -rf node_modules dist dist-esm", "test": "pnpm exec jest --forceExit --runInBand", "depcheck": "pnpx depcheck" }, diff --git a/src/packages/sync-fs/tsconfig-esm.json b/src/packages/sync-fs/tsconfig-esm.json new file mode 100644 index 00000000000..a78c78ec542 --- /dev/null +++ b/src/packages/sync-fs/tsconfig-esm.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/sync-fs/tsconfig.json b/src/packages/sync-fs/tsconfig.json index 2290a242a58..c700fa3faa7 100644 --- a/src/packages/sync-fs/tsconfig.json +++ b/src/packages/sync-fs/tsconfig.json @@ -4,7 +4,7 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references": [ { "path": "../api-client" }, { "path": "../sync-client" }, diff --git a/src/packages/sync/package.json b/src/packages/sync/package.json index fbe9669a4cd..f0937901f89 100644 --- a/src/packages/sync/package.json +++ b/src/packages/sync/package.json @@ -3,20 +3,37 @@ "version": "0.11.2", "description": "CoCalc realtime synchronization framework", "exports": { - "./*": "./dist/*.js", - "./table": "./dist/table/index.js", - "./listings": "./dist/listings/index.js", - "./editor/db": "./dist/editor/db/index.js" + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./table": { + "types": "./dist/table/index.d.ts", + "import": "./dist-esm/table/index.js", + "default": "./dist/table/index.js" + }, + "./listings": { + "types": "./dist/listings/index.d.ts", + "import": "./dist-esm/listings/index.js", + "default": "./dist/listings/index.js" + }, + "./editor/db": { + "types": "./dist/editor/db/index.d.ts", + "import": "./dist-esm/editor/db/index.js", + "default": "./dist/editor/db/index.js" + } }, "scripts": { "preinstall": "npx only-allow pnpm", - "build": "../node_modules/.bin/tsc --build", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", "test": "pnpm exec jest --forceExit", "depcheck": "pnpx depcheck --ignores events", "prepublishOnly": "pnpm test" }, "files": [ "dist/**", + "dist-esm/**", "bin/**", "README.md", "package.json" diff --git a/src/packages/sync/tsconfig-esm.json b/src/packages/sync/tsconfig-esm.json new file mode 100644 index 00000000000..0f19e5a2d51 --- /dev/null +++ b/src/packages/sync/tsconfig-esm.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true, + "paths": { + "@cocalc/sync/*": ["./*"] + } + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/sync/tsconfig.json b/src/packages/sync/tsconfig.json index 0bdfd8b3a42..9692649d113 100644 --- a/src/packages/sync/tsconfig.json +++ b/src/packages/sync/tsconfig.json @@ -4,6 +4,6 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "dist", "test"], + "exclude": ["node_modules", "dist", "dist-esm", "test"], "references": [{ "path": "../util" }, { "path": "../conat" }] } diff --git a/src/packages/util/heartbeat.js b/src/packages/util/heartbeat.js deleted file mode 100644 index 835ad533942..00000000000 --- a/src/packages/util/heartbeat.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. - * License: MS-RSL – see LICENSE.md for details - */ - -exports.PROJECT_HUB_HEARTBEAT_INTERVAL_S = 30; diff --git a/src/packages/util/heartbeat.ts b/src/packages/util/heartbeat.ts new file mode 100644 index 00000000000..58a0d987991 --- /dev/null +++ b/src/packages/util/heartbeat.ts @@ -0,0 +1,6 @@ +/* + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. + * License: MS-RSL – see LICENSE.md for details + */ + +export const PROJECT_HUB_HEARTBEAT_INTERVAL_S = 30; diff --git a/src/packages/util/immutable-types.js b/src/packages/util/immutable-types.ts similarity index 78% rename from src/packages/util/immutable-types.js rename to src/packages/util/immutable-types.ts index bdadc2f8f42..9f0d4f88d1c 100644 --- a/src/packages/util/immutable-types.js +++ b/src/packages/util/immutable-types.ts @@ -1,20 +1,9 @@ /* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. * License: MS-RSL – see LICENSE.md for details */ -/* - * decaffeinate suggestions: - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ -//############################################################################## -// -// CoCalc: Collaborative web-based calculation -// Copyright (C) 2017, Sagemath Inc. -// MS-RSL -// -//############################################################################## +import * as Immutable from "immutable"; /* Custom Prop Validation for immutable.js types, so they work just like other @@ -48,8 +37,8 @@ const check_is_immutable = function ( props, propName, componentName, - location, - propFullName + _location?, + _propFullName?, ) { if (componentName == null) { componentName = "ANONYMOUS"; @@ -61,7 +50,7 @@ const check_is_immutable = function ( return new Error( `Invalid prop \`${propName}\` of` + ` type ${type} supplied to` + - ` \`${componentName}\`, expected an immutable collection or frozen object.` + ` \`${componentName}\`, expected an immutable collection or frozen object.`, ); } }; @@ -72,14 +61,14 @@ const allow_isRequired = function (validate) { props, propName, componentName, - location + location, ) { if (componentName == null) { componentName = "ANONYMOUS"; } if (props[propName] == null && isRequired) { return new Error( - `Required prop \`${propName}\` was not specified in \`${componentName}\`` + `Required prop \`${propName}\` was not specified in \`${componentName}\``, ); } return validate(props, propName, componentName, location); @@ -98,7 +87,7 @@ const create_immutable_type_required_chain = function (validate) { immutable_type_name, props, propName, - componentName + componentName, ) { if (componentName == null) { componentName = "ANONYMOUS"; @@ -107,16 +96,16 @@ const create_immutable_type_required_chain = function (validate) { const T = immutable_type_name; if (props[propName].toJS == null) { return new Error( - `NOT EVEN IMMUTABLE, wanted immutable.${T} ${props}, ${propName}` + `NOT EVEN IMMUTABLE, wanted immutable.${T} ${props}, ${propName}`, ); } - if (require("immutable")[`${T}`][`is${T}`](props[propName])) { + if (Immutable[`${T}`][`is${T}`](props[propName])) { return null; } else { return new Error( `Component \`${componentName}\`` + ` expected ${propName} to be an immutable.${T}` + - ` but was supplied ${props[propName]}` + ` but was supplied ${props[propName]}`, ); } } else { @@ -126,17 +115,18 @@ const create_immutable_type_required_chain = function (validate) { // To add more immutable.js types, mimic code below. const check_immutable_chain = allow_isRequired( - check_type.bind(null, undefined) + check_type.bind(null, undefined), ); check_immutable_chain.Map = allow_isRequired(check_type.bind(null, "Map")); check_immutable_chain.List = allow_isRequired(check_type.bind(null, "List")); check_immutable_chain.Set = allow_isRequired(check_type.bind(null, "Set")); check_immutable_chain.Stack = allow_isRequired( - check_type.bind(null, "Stack") + check_type.bind(null, "Stack"), ); check_immutable_chain.category = "IMMUTABLE"; return check_immutable_chain; }; -exports.immutable = create_immutable_type_required_chain(check_is_immutable); +export const immutable = + create_immutable_type_required_chain(check_is_immutable); diff --git a/src/packages/util/mathjax-config.js b/src/packages/util/mathjax-config.ts similarity index 96% rename from src/packages/util/mathjax-config.js rename to src/packages/util/mathjax-config.ts index 487cd673dd2..1e97f52c022 100644 --- a/src/packages/util/mathjax-config.js +++ b/src/packages/util/mathjax-config.ts @@ -1,12 +1,12 @@ /* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. * License: MS-RSL – see LICENSE.md for details */ // mathjax configuration: this could be cleaned up further or even parameterized with some code during startup // ATTN: do not use "xypic.js", frequently causes crash! -exports.MathJaxConfig = { +export const MathJaxConfig = { skipStartupTypeset: true, extensions: ["tex2jax.js", "asciimath2jax.js", "Safe.js"], // "static/mathjax_extensions/xypic.js" // NOTE: "output/CommonHTML" is the output default: http://docs.mathjax.org/en/latest/output.html diff --git a/src/packages/util/mathjax-utils-2.js b/src/packages/util/mathjax-utils-2.ts similarity index 88% rename from src/packages/util/mathjax-utils-2.js rename to src/packages/util/mathjax-utils-2.ts index eaded15324d..8af099c7dc7 100644 --- a/src/packages/util/mathjax-utils-2.js +++ b/src/packages/util/mathjax-utils-2.ts @@ -1,5 +1,5 @@ /* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. * License: MS-RSL – see LICENSE.md for details */ diff --git a/src/packages/util/mathjax-utils.d.ts b/src/packages/util/mathjax-utils.d.ts deleted file mode 100644 index 06e5f458d8f..00000000000 --- a/src/packages/util/mathjax-utils.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const replace_math: Function; -export const remove_math: Function; diff --git a/src/packages/util/mathjax-utils.js b/src/packages/util/mathjax-utils.ts similarity index 89% rename from src/packages/util/mathjax-utils.js rename to src/packages/util/mathjax-utils.ts index a631e961a41..942222a8d2b 100644 --- a/src/packages/util/mathjax-utils.js +++ b/src/packages/util/mathjax-utils.ts @@ -1,5 +1,5 @@ /* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. * License: MS-RSL – see LICENSE.md for details */ @@ -21,8 +21,7 @@ const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|(?:\n\s*)+)/i; // Jupyter classic doesn't and it conflicts too much with markdown. Use $'s and e.g., \begin{equation}. // const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|(?:\n\s*)+|\\(?:\(|\)|\[|\]))/i; -// This runs under node.js and is js (not ts) so can't use import. -const { regex_split } = require("./regex-split"); +import { regex_split } from "./regex-split"; // The math is in blocks i through j, so // collect it into one block and clear the others. @@ -56,8 +55,7 @@ function process_math(i, j, pre_process, math, blocks, tags) { // // Do *NOT* conflict with the ones used in ./markdown-utils.ts -const MATH_ESCAPE = "\uFE32\uFE33"; // unused unicode -- hardcoded below too -exports.MATH_ESCAPE = MATH_ESCAPE; +export const MATH_ESCAPE = "\uFE32\uFE33"; // unused unicode -- hardcoded below too const DEFAULT_TAGS = { open: MATH_ESCAPE, @@ -66,12 +64,12 @@ const DEFAULT_TAGS = { display_close: MATH_ESCAPE, }; -function remove_math(text, tags = DEFAULT_TAGS) { - let math = []; // stores math strings for later - let start; - let end; - let last; - let braces; +export function remove_math(text: string, tags = DEFAULT_TAGS) { + let math: string[] = []; // stores math strings for later + let start: number | null = null; + let end: string | null = null; + let last: number | null = null; + let braces: number = 0; // Except for extreme edge cases, this should catch precisely those pieces of the markdown // source that will later be turned into code spans. While MathJax will not TeXify code spans, @@ -88,7 +86,7 @@ function remove_math(text, tags = DEFAULT_TAGS) { return wholematch.replace(/\$/g, "~D"); }); de_tilde = function (text) { - return text.replace(/~([TD])/g, function (wholematch, character) { + return text.replace(/~([TD])/g, function (_wholematch, character) { return { T: "~", D: "$" }[character]; }); }; @@ -101,7 +99,7 @@ function remove_math(text, tags = DEFAULT_TAGS) { let blocks = regex_split(text.replace(/\r\n?/g, "\n"), MATHSPLIT); for (let i = 1, m = blocks.length; i < m; i += 2) { - const block = blocks[i]; + const block = blocks[i] as string; if (start) { // // If we are in math, look for the end delimiter, @@ -159,17 +157,16 @@ function remove_math(text, tags = DEFAULT_TAGS) { } return [de_tilde(blocks.join("")), math]; } -exports.remove_math = remove_math; // // Put back the math strings that were saved. // -function replace_math(text, math, tags) { +export function replace_math(text, math, tags?) { // Replace all the math group placeholders in the text // with the saved strings. if (tags == null) { // Easy to do with a regexp - return text.replace(/\uFE32\uFE33(\d+)\uFE32\uFE33/g, function (match, n) { + return text.replace(/\uFE32\uFE33(\d+)\uFE32\uFE33/g, function (_match, n) { return math[n]; }); } else { @@ -197,5 +194,3 @@ function replace_math(text, math, tags) { return text; } } - -exports.replace_math = replace_math; diff --git a/src/packages/util/message.d.ts b/src/packages/util/message.d.ts deleted file mode 100644 index 2682106b9ac..00000000000 --- a/src/packages/util/message.d.ts +++ /dev/null @@ -1,105 +0,0 @@ -export const start_session: any; -export const session_started: any; -export const output: any; -export const execute_javascript: any; -export const usernames: any; -export const create_account: any; -export const account_created: any; -export const account_creation_failed: any; -export const delete_account: any; -export const account_deleted: any; -export const sign_in: any; -export const remember_me_failed: any; -export const sign_in_failed: any; -export const signed_in: any; -export const sign_out: any; -export const signed_out: any; -export const error: any; -export const success: any; -export const reconnect: any; -export const cookies: any; -export const open_project: any; -export const project_opened: any; -export const project_exec: any; -export const project_exec_output: any; -export const named_server_port: any; -export const read_file_from_project: any; -export const file_read_from_project: any; -export const read_text_file_from_project: any; -export const text_file_read_from_project: any; -export const write_file_to_project: any; -export const write_text_file_to_project: any; -export const file_written_to_project: any; -export const project_users: any; -export const version: any; -export const save_blob: any; -export const storage: any; -export const projects_running_on_server: any; -export const local_hub: any; -export const copy_path_between_projects: any; -export const copy_path_between_projects_response: any; -export const copy_path_status: any; -export const copy_path_status_response: any; -export const copy_path_delete: any; -export const print_to_pdf: any; -export const printed_to_pdf: any; -export const heartbeat: any; -export const ping: any; -export const pong: any; -export const log_client_error: any; -export const webapp_error: any; -export const stripe_get_customer: any; -export const stripe_customer: any; -export const stripe_create_source: any; -export const stripe_delete_source: any; -export const stripe_set_default_source: any; -export const stripe_update_source: any; -export const stripe_plans: any; -export const stripe_create_subscription: any; -export const stripe_cancel_subscription: any; -export const stripe_update_subscription: any; -export const stripe_get_subscriptions: any; -export const stripe_subscriptions: any; -export const stripe_get_coupon: any; -export const stripe_coupon: any; -export const stripe_get_charges: any; -export const stripe_charges: any; -export const stripe_get_invoices: any; -export const stripe_invoices: any; -export const stripe_admin_create_invoice_item: any; -export const query: any; -export const uuid: any; -export const uuid: any; -export const query_cancel: any; -export const api_key: any; -export const api_key_info: any; -export const api_keys: any; -export const api_keys_response: any; -export const user_auth: any; -export const user_auth_token: any; -export const get_available_upgrades: any; -export const disconnect_from_project: any; -export const available_upgrades: any; -export const remove_all_upgrades: any; -export const sagews_execute_code: any; -export const sagews_output: any; -export const sagews_output_ack: any; -export const sagews_interrupt: any; -export const sagews_quit: any; -export const sagews_start: any; -export const user_tracking: any; -export const purchase_license: any; -export const purchase_license_resp: any; -export const chatgpt: any; -export const chatgpt_response: any; -export const openai_embeddings_search: any; -export const openai_embeddings_search_response: any; -export const openai_embeddings_save: any; -export const openai_embeddings_save_response: any; -export const openai_embeddings_remove: any; -export const openai_embeddings_remove_response: any; -export const jupyter_execute: any; -export const jupyter_execute_response: any; -export const jupyter_kernels: any; -export const jupyter_start_pool: any; -export const signal_sent: any; diff --git a/src/packages/util/message.js b/src/packages/util/message.ts similarity index 94% rename from src/packages/util/message.js rename to src/packages/util/message.ts index 5aea5143a8e..2ca78039486 100644 --- a/src/packages/util/message.js +++ b/src/packages/util/message.ts @@ -1,5 +1,5 @@ /* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. + * This file is part of CoCalc: Copyright © 2020-2026 Sagemath, Inc. * License: MS-RSL – see LICENSE.md for details */ @@ -96,19 +96,17 @@ If id is not provided in the API message, a random id will be generated and returned in the response.\ `; -const misc = require("./misc"); -const { defaults } = misc; +import { defaults, to_json } from "./misc"; +import mapValues from "lodash/mapValues"; + const { required } = defaults; -const _ = require("underscore"); -function message(obj) { - exports[obj.event] = function (opts, strict) { - if (opts == null) { - opts = {}; - } - if (strict == null) { - strict = false; - } +// All message factories are collected here at runtime. +// IMPORTANT: these are unpacked as named exports at the bottom of this file. +const messages: Record = {}; + +function message(obj: any) { + messages[obj.event] = function (opts: any = {}, strict = false) { if (opts.event != null) { throw Error( `ValueError: must not define 'event' when calling message creation function (opts=${JSON.stringify( @@ -121,50 +119,50 @@ function message(obj) { return obj; } +// messages that can be used by the HTTP api. {'event':true, ...} +export const api_messages: Record = {}; + +// this holds the documentation for the message protocol +export const documentation: { intro: string; events: Record } = { + intro: doc_intro, + events: {}, +}; + +// holds all the examples: list of expected in/out objects for each message +export const examples: Record = {}; + // message2 for "version 2" of the message definitions // TODO document it, for now just search for "message2" to see examples -function message2(obj) { - function mk_desc(val) { +function message2(obj: any) { + function mk_desc(val: any) { let { desc } = val; if (val.init === required) { desc += " (required)"; } else if (val.init != null) { - desc += ` (default: ${misc.to_json(val.init)})`; + desc += ` (default: ${to_json(val.init)})`; } return desc; } // reassembling a version 1 message from a version 2 message - const mesg_v1 = _.mapObject(obj.fields, (val) => val.init); + const mesg_v1: any = mapValues(obj.fields, (val: any) => val.init); mesg_v1.event = obj.event; // extracting description for the documentation - const fdesc = _.mapObject(obj.fields, mk_desc); - exports.documentation.events[obj.event] = { + const fdesc = mapValues(obj.fields, mk_desc); + documentation.events[obj.event] = { description: obj.desc != null ? obj.desc : "", fields: fdesc, }; // ... and the examples - exports.examples[obj.event] = obj.examples; + examples[obj.event] = obj.examples; // wrapped version 1 message message(mesg_v1); return obj; } -// messages that can be used by the HTTP api. {'event':true, ...} -exports.api_messages = {}; - -// this holds the documentation for the message protocol -exports.documentation = { - intro: doc_intro, - events: {}, -}; - -// holds all the examples: list of expected in/out objects for each message -exports.examples = {}; - -const API = (obj) => +const API = (obj: any) => // obj could be message version 1 or 2! - (exports.api_messages[obj.event] = true); + (api_messages[obj.event] = true); //########################################### // Sage session management; executing code @@ -2038,3 +2036,120 @@ API( kernels: undefined, // response is same message but with this filled in with array of data giving available kernels }), ); + +// Named exports for all message factories registered above. +// When adding a new message() call, also add its event name here. +export const { + account_created, + account_creation_failed, + account_deleted, + api_key, + api_key_info, + api_keys, + api_keys_response, + available_upgrades, + chatgpt, + chatgpt_response, + cookies, + copy_path_between_projects, + copy_path_between_projects_response, + copy_path_delete, + copy_path_status, + copy_path_status_response, + create_account, + delete_account, + disconnect_from_project, + error, + execute_javascript, + file_read_from_project, + file_written_to_project, + get_available_upgrades, + heartbeat, + jupyter_execute, + jupyter_execute_response, + jupyter_kernels, + jupyter_start_pool, + local_hub, + log_client_error, + named_server_port, + openai_embeddings_remove, + openai_embeddings_remove_response, + openai_embeddings_save, + openai_embeddings_save_response, + openai_embeddings_search, + openai_embeddings_search_response, + open_project, + output, + ping, + pong, + print_to_pdf, + printed_to_pdf, + project_exec, + project_exec_output, + project_opened, + project_users, + projects_running_on_server, + purchase_license, + purchase_license_resp, + query, + query_cancel, + read_file_from_project, + read_text_file_from_project, + reconnect, + remember_me_failed, + remove_all_upgrades, + sagews_execute_code, + sagews_interrupt, + sagews_output, + sagews_output_ack, + sagews_quit, + sagews_start, + save_blob, + session_started, + sign_in, + sign_in_failed, + sign_out, + signed_in, + signed_out, + start_session, + stripe_admin_create_invoice_item, + stripe_cancel_subscription, + stripe_charges, + stripe_coupon, + stripe_create_source, + stripe_create_subscription, + stripe_customer, + stripe_delete_source, + stripe_get_charges, + stripe_get_coupon, + stripe_get_customer, + stripe_get_invoices, + stripe_get_subscriptions, + stripe_invoices, + stripe_plans, + stripe_set_default_source, + stripe_subscriptions, + stripe_update_source, + stripe_update_subscription, + success, + text_file_read_from_project, + user_auth, + user_search_results, + user_tracking, + version, + webapp_error, + write_file_to_project, + write_text_file_to_project, +} = messages; + +// Safeguard: fail at load time if a message() call was added above +// but its event name was not added to the export list. +// In CJS, tsc compiles named exports to properties on `exports`. +for (const key of Object.keys(messages)) { + // @ts-ignore -- accessing CJS exports object directly for runtime check + if (typeof exports[key] !== "function") { + throw Error( + `message.ts: "${key}" is registered via message() but missing from the named export list at the bottom of this file. Add it there.`, + ); + } +} diff --git a/src/packages/util/package.json b/src/packages/util/package.json index f7d21c4d8ea..c94bed17a7c 100644 --- a/src/packages/util/package.json +++ b/src/packages/util/package.json @@ -3,27 +3,68 @@ "version": "1.77.2", "description": "CoCalc code shared between the frontend and the backend", "exports": { - "./*": "./dist/*.js", - "./db-schema": "./dist/db-schema/index.js", - "./fill": "./dist/fill/index.js", - "./types": "./dist/types/index.js", - "./consts": "./dist/consts/index.js", - "./i18n": "./dist/i18n/index.js", - "./sync/table": "./dist/sync/table/index.js", - "./sync/editor/db": "./dist/sync/editor/db/index.js", - "./licenses/purchase/*": "./dist/licenses/purchase/*.js", - "./redux/*": "./dist/redux/*.js" + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist-esm/*.js", + "default": "./dist/*.js" + }, + "./db-schema": { + "types": "./dist/db-schema/index.d.ts", + "import": "./dist-esm/db-schema/index.js", + "default": "./dist/db-schema/index.js" + }, + "./fill": { + "types": "./dist/fill/index.d.ts", + "import": "./dist-esm/fill/index.js", + "default": "./dist/fill/index.js" + }, + "./types": { + "types": "./dist/types/index.d.ts", + "import": "./dist-esm/types/index.js", + "default": "./dist/types/index.js" + }, + "./consts": { + "types": "./dist/consts/index.d.ts", + "import": "./dist-esm/consts/index.js", + "default": "./dist/consts/index.js" + }, + "./i18n": { + "types": "./dist/i18n/index.d.ts", + "import": "./dist-esm/i18n/index.js", + "default": "./dist/i18n/index.js" + }, + "./sync/table": { + "types": "./dist/sync/table/index.d.ts", + "import": "./dist-esm/sync/table/index.js", + "default": "./dist/sync/table/index.js" + }, + "./sync/editor/db": { + "types": "./dist/sync/editor/db/index.d.ts", + "import": "./dist-esm/sync/editor/db/index.js", + "default": "./dist/sync/editor/db/index.js" + }, + "./licenses/purchase/*": { + "types": "./dist/licenses/purchase/*.d.ts", + "import": "./dist-esm/licenses/purchase/*.js", + "default": "./dist/licenses/purchase/*.js" + }, + "./redux/*": { + "types": "./dist/redux/*.d.ts", + "import": "./dist-esm/redux/*.js", + "default": "./dist/redux/*.js" + } }, "scripts": { "preinstall": "npx only-allow pnpm", - "clean": "rm -rf node_modules dist", - "build": "pnpm exec tsc --build", + "clean": "rm -rf node_modules dist dist-esm", + "build": "pnpm exec tsc --build && pnpm exec tsc -p tsconfig-esm.json && echo '{\"type\":\"module\"}' > dist-esm/package.json", "test": "pnpm exec jest", "depcheck": "pnpx depcheck --ignores events", "prepublishOnly": "pnpm test" }, "files": [ "dist/**", + "dist-esm/**", "bin/**", "README.md", "package.json" @@ -60,7 +101,6 @@ "request-ip": "^3.3.0", "reselect": "^4.1.8", "sha1": "^1.1.1", - "underscore": "^1.12.1", "utility-types": "^3.10.0", "uuid": "^8.3.2", "voucher-code-generator": "^1.3.0" diff --git a/src/packages/util/quota.test.ts b/src/packages/util/quota.test.ts index fe9b29082c4..014d1bfd39a 100644 --- a/src/packages/util/quota.test.ts +++ b/src/packages/util/quota.test.ts @@ -13,7 +13,7 @@ extend it to test whatever you changed too. In one terminal: and in another: -.../packages/util$ ../node_modules/.bin/jest dist/quota.test.js [--watch] +.../packages/util$ pnpm exec jest dist/quota.test.js [--watch] Also generally do this: diff --git a/src/packages/util/regex-split.js b/src/packages/util/regex-split.js deleted file mode 100644 index 352affbbfb8..00000000000 --- a/src/packages/util/regex-split.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. - * License: MS-RSL – see LICENSE.md for details - */ - -//============================================================================ -// Cross-browser RegEx Split -//============================================================================ - -// This code has been MODIFIED from the code licensed below to not replace the -// default browser split. The license is reproduced here. - -// see http://blog.stevenlevithan.com/archives/cross-browser-split for more info: -/*! - * Cross-Browser Split 1.1.1 - * Copyright 2007-2012 Steven Levithan - * Available under the MIT License - * ECMAScript compliant, uniform cross-browser split method - */ - -/** - * Splits a string into an array of strings using a regex or string - * separator. Matches of the separator are not included in the result array. - * However, if `separator` is a regex that contains capturing groups, - * backreferences are spliced into the result each time `separator` is - * matched. Fixes browser bugs compared to the native - * `String.prototype.split` and can be used reliably cross-browser. - * @param {String} str String to split. - * @param {RegExp} separator Regex to use for separating - * the string. - * @param {Number} [limit] Maximum number of items to include in the result - * array. - * @returns {Array} Array of substrings. - * @example - * - * // Basic use - * regex_split('a b c d', ' '); - * // -> ['a', 'b', 'c', 'd'] - * - * // With limit - * regex_split('a b c d', ' ', 2); - * // -> ['a', 'b'] - * - * // Backreferences in result array - * regex_split('..word1 word2..', /([a-z]+)(\d+)/i); - * // -> ['..', 'word', '1', ' ', 'word', '2', '..'] - */ -exports.regex_split = function (str, separator, limit) { - var output = [], - flags = - (separator.ignoreCase ? "i" : "") + - (separator.multiline ? "m" : "") + - (separator.extended ? "x" : "") + // Proposed for ES6 - (separator.sticky ? "y" : ""), // Firefox 3+ - lastLastIndex = 0, - separator2, - match, - lastIndex, - lastLength; - // Make `global` and avoid `lastIndex` issues by working with a copy - separator = new RegExp(separator.source, flags + "g"); - - var compliantExecNpcg = typeof /()??/.exec("")[1] === "undefined"; - if (!compliantExecNpcg) { - // Doesn't need flags gy, but they don't hurt - separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags); - } - /* Values for `limit`, per the spec: - * If undefined: 4294967295 // Math.pow(2, 32) - 1 - * If 0, Infinity, or NaN: 0 - * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; - * If negative number: 4294967296 - Math.floor(Math.abs(limit)) - * If other: Type-convert, then use the above rules - */ - limit = - typeof limit === "undefined" - ? -1 >>> 0 // Math.pow(2, 32) - 1 - : limit >>> 0; // ToUint32(limit) - for (match = separator.exec(str); match; match = separator.exec(str)) { - // `separator.lastIndex` is not reliable cross-browser - lastIndex = match.index + match[0].length; - if (lastIndex > lastLastIndex) { - output.push(str.slice(lastLastIndex, match.index)); - // Fix browsers whose `exec` methods don't consistently return `undefined` for - // nonparticipating capturing groups - if (!compliantExecNpcg && match.length > 1) { - match[0].replace(separator2, function () { - for (var i = 1; i < arguments.length - 2; i++) { - if (typeof arguments[i] === "undefined") { - match[i] = undefined; - } - } - }); - } - if (match.length > 1 && match.index < str.length) { - Array.prototype.push.apply(output, match.slice(1)); - } - lastLength = match[0].length; - lastLastIndex = lastIndex; - if (output.length >= limit) { - break; - } - } - if (separator.lastIndex === match.index) { - separator.lastIndex++; // Avoid an infinite loop - } - } - if (lastLastIndex === str.length) { - if (lastLength || !separator.test("")) { - output.push(""); - } - } else { - output.push(str.slice(lastLastIndex)); - } - return output.length > limit ? output.slice(0, limit) : output; -}; - -//============================================================================ -// End contributed Cross-browser RegEx Split -//============================================================================ diff --git a/src/packages/util/regex-split.test.ts b/src/packages/util/regex-split.test.ts new file mode 100644 index 00000000000..8e967a0c85b --- /dev/null +++ b/src/packages/util/regex-split.test.ts @@ -0,0 +1,96 @@ +/* + * This file is part of CoCalc: Copyright © 2026 Sagemath, Inc. + * License: MS-RSL – see LICENSE.md for details + */ + +import { regex_split } from "./regex-split"; + +describe("regex_split", () => { + test("basic split on space", () => { + expect(regex_split("a b c d", / /)).toEqual(["a", "b", "c", "d"]); + }); + + test("split with limit", () => { + expect(regex_split("a b c d", / /, 2)).toEqual(["a", "b"]); + }); + + test("split with capturing groups includes backreferences", () => { + expect(regex_split("..word1 word2..", /([a-z]+)(\d+)/i)).toEqual([ + "..", + "word", + "1", + " ", + "word", + "2", + "..", + ]); + }); + + test("empty string returns array with empty string", () => { + expect(regex_split("", /,/)).toEqual([""]); + }); + + test("no match returns original string in array", () => { + expect(regex_split("hello", /,/)).toEqual(["hello"]); + }); + + test("consecutive separators produce empty strings", () => { + expect(regex_split("a,,b", /,/)).toEqual(["a", "", "b"]); + }); + + test("separator at start", () => { + expect(regex_split(",a,b", /,/)).toEqual(["", "a", "b"]); + }); + + test("separator at end", () => { + expect(regex_split("a,b,", /,/)).toEqual(["a", "b", ""]); + }); + + test("case-insensitive flag is preserved", () => { + expect(regex_split("aXbXc", /x/i)).toEqual(["a", "b", "c"]); + }); + + test("multiline flag is preserved", () => { + const result = regex_split("a\nb\nc", /^/m); + expect(result).toEqual(["a\n", "b\n", "c"]); + }); + + test("limit of 0 returns empty array", () => { + expect(regex_split("a,b,c", /,/, 0)).toEqual([]); + }); + + test("limit of 1 returns first part", () => { + expect(regex_split("a,b,c", /,/, 1)).toEqual(["a"]); + }); + + test("limit larger than splits returns all parts", () => { + expect(regex_split("a,b", /,/, 100)).toEqual(["a", "b"]); + }); + + test("split on alternation (MATHSPLIT-like pattern)", () => { + // Mimics the MathJax MATHSPLIT pattern for $ delimiters + const MATHSPLIT = /(\$\$?)/; + expect(regex_split("text $math$ more", MATHSPLIT)).toEqual([ + "text ", + "$", + "math", + "$", + " more", + ]); + }); + + test("split with \\begin/\\end pattern", () => { + const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\})/i; + const result = regex_split( + "before\\begin{align}x=1\\end{align}after", + MATHSPLIT, + ); + expect(result).toEqual([ + "before", + "\\begin{align}", + "x=1", + "\\end{align}", + "after", + ]); + }); +}); diff --git a/src/packages/util/regex-split.ts b/src/packages/util/regex-split.ts new file mode 100644 index 00000000000..250b4638d4b --- /dev/null +++ b/src/packages/util/regex-split.ts @@ -0,0 +1,128 @@ +/* + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. + * License: MS-RSL – see LICENSE.md for details + */ + +//============================================================================ +// Cross-browser RegEx Split +//============================================================================ + +// This code has been MODIFIED from the code licensed below to not replace the +// default browser split. The license is reproduced here. + +// see http://blog.stevenlevithan.com/archives/cross-browser-split for more info: +/*! + * Cross-Browser Split 1.1.1 + * Copyright 2007-2012 Steven Levithan + * Available under the MIT License + * ECMAScript compliant, uniform cross-browser split method + */ + +/** + * Splits a string into an array of strings using a regex separator. + * Matches of the separator are not included in the result array. + * However, if `separator` is a regex that contains capturing groups, + * backreferences are spliced into the result each time `separator` is + * matched. Fixes browser bugs compared to the native + * `String.prototype.split` and can be used reliably cross-browser. + * + * @example + * regex_split('a b c d', / /); + * // -> ['a', 'b', 'c', 'd'] + * + * @example + * regex_split('a b c d', / /, 2); + * // -> ['a', 'b'] + * + * @example + * regex_split('..word1 word2..', /([a-z]+)(\d+)/i); + * // -> ['..', 'word', '1', ' ', 'word', '2', '..'] + */ +export function regex_split( + str: string, + separator: RegExp, + limit?: number, +): (string | undefined)[] { + const output: (string | undefined)[] = []; + + const flags = + (separator.ignoreCase ? "i" : "") + + (separator.multiline ? "m" : "") + + ((separator as any).extended ? "x" : "") + // Proposed for ES6 + (separator.sticky ? "y" : ""); // Firefox 3+ + + // Make `global` and avoid `lastIndex` issues by working with a copy + const globalSeparator = new RegExp(separator.source, flags + "g"); + + let lastLastIndex = 0; + let lastLength: number | undefined; + + // For non-compliant browsers that don't return `undefined` for + // nonparticipating capturing groups. + const compliantExecNpcg = typeof /()??/.exec("")![1] === "undefined"; + let separator2: RegExp | undefined; + if (!compliantExecNpcg) { + separator2 = new RegExp("^" + globalSeparator.source + "$(?!\\s)", flags); + } + + // Values for `limit`, per the spec: + // If undefined: 4294967295 // Math.pow(2, 32) - 1 + // If 0, Infinity, or NaN: 0 + // If positive number: limit = Math.floor(limit); ... + // If negative number: 4294967296 - Math.floor(Math.abs(limit)) + // If other: Type-convert, then use the above rules + const effectiveLimit: number = + typeof limit === "undefined" + ? -1 >>> 0 // Math.pow(2, 32) - 1 + : limit >>> 0; // ToUint32(limit) + + let match: RegExpExecArray | null; + for ( + match = globalSeparator.exec(str); + match; + match = globalSeparator.exec(str) + ) { + // `separator.lastIndex` is not reliable cross-browser + const lastIndex = match.index + match[0].length; + if (lastIndex > lastLastIndex) { + output.push(str.slice(lastLastIndex, match.index)); + // Fix browsers whose `exec` methods don't consistently return `undefined` + // for nonparticipating capturing groups + if (!compliantExecNpcg && match.length > 1) { + match[0].replace(separator2!, function (...args: any[]): string { + for (let i = 1; i < args.length - 2; i++) { + if (typeof args[i] === "undefined") { + match![i] = undefined as any; + } + } + return ""; + }); + } + if (match.length > 1 && match.index < str.length) { + Array.prototype.push.apply(output, match.slice(1)); + } + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= effectiveLimit) { + break; + } + } + if (globalSeparator.lastIndex === match.index) { + globalSeparator.lastIndex++; // Avoid an infinite loop + } + } + if (lastLastIndex === str.length) { + if (lastLength || !globalSeparator.test("")) { + output.push(""); + } + } else { + output.push(str.slice(lastLastIndex)); + } + return output.length > effectiveLimit + ? output.slice(0, effectiveLimit) + : output; +} + +//============================================================================ +// End contributed Cross-browser RegEx Split +//============================================================================ diff --git a/src/packages/util/smc-version.js b/src/packages/util/smc-version.ts similarity index 58% rename from src/packages/util/smc-version.js rename to src/packages/util/smc-version.ts index 44ef81468b6..3c42cd09b65 100644 --- a/src/packages/util/smc-version.js +++ b/src/packages/util/smc-version.ts @@ -1,2 +1,2 @@ /* autogenerated by the update_version script */ -exports.version=1763609578; +export const version = 1771527616; diff --git a/src/packages/util/tsconfig-esm.json b/src/packages/util/tsconfig-esm.json new file mode 100644 index 00000000000..32df14142bf --- /dev/null +++ b/src/packages/util/tsconfig-esm.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "./", + "outDir": "dist-esm", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": false, + "declarationMap": false, + "composite": false, + "incremental": false, + "sourceMap": true, + "paths": { + "@cocalc/util/*": ["./*"] + } + }, + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] +} diff --git a/src/packages/util/tsconfig.json b/src/packages/util/tsconfig.json index 95a218962f3..7870d144938 100644 --- a/src/packages/util/tsconfig.json +++ b/src/packages/util/tsconfig.json @@ -4,5 +4,5 @@ "rootDir": "./", "outDir": "dist" }, - "exclude": ["node_modules", "../node_modules", "dist", "test"] + "exclude": ["node_modules", "../node_modules", "dist", "dist-esm", "test"] } diff --git a/src/packages/util/upgrades.js b/src/packages/util/upgrades.ts similarity index 79% rename from src/packages/util/upgrades.js rename to src/packages/util/upgrades.ts index 1d98ecd5594..0bab6544dd3 100644 --- a/src/packages/util/upgrades.js +++ b/src/packages/util/upgrades.ts @@ -1,16 +1,15 @@ /* - * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. + * This file is part of CoCalc: Copyright © 2020–2026 Sagemath, Inc. * License: MS-RSL – see LICENSE.md for details */ -const { PROJECT_UPGRADES } = require("./schema"); - -const misc = require("./misc"); +import { PROJECT_UPGRADES } from "./schema"; +import { cmp, map_sum } from "./misc"; // This is used by the frontend in r_account. It's also used by the backend // to double check the claims of the frontend. // stripe_subscriptions_data = stripe_customer?.subscriptions?.data -function get_total_upgrades(stripe_subscriptions_data, DEBUG = false) { +export function get_total_upgrades(stripe_subscriptions_data, DEBUG = false) { if (DEBUG) { return PROJECT_UPGRADES.subscription.professional.benefits; } @@ -22,7 +21,7 @@ function get_total_upgrades(stripe_subscriptions_data, DEBUG = false) { // always running isn't in any of the subscriptions and I don't want to edit benefits // for all of them, so just put a zero 0 (since this whole old upgrades thing // will eventually go away). https://github.com/sagemathinc/cocalc/issues/4802 - let total = { always_running: 0 }; + let total: Record = { always_running: 0 }; for (let sub of subs) { if (sub.status != "active" && sub.status != "trialing") { // not yet paid for or no longer available. @@ -35,14 +34,12 @@ function get_total_upgrades(stripe_subscriptions_data, DEBUG = false) { } const benefits = info.benefits; for (let q = 0; q < sub.quantity; q++) { - total = misc.map_sum(total, benefits); + total = map_sum(total, benefits); } } return total; } -exports.get_total_upgrades = get_total_upgrades; - // // INPUT: // subscriptions = {standard:2, premium:1, course:2, ...} @@ -51,17 +48,17 @@ exports.get_total_upgrades = get_total_upgrades; // OUTPUT: // {available:{cores:10, network:3, ...}, excess:{project_id:{cores:2, ...}} } // -function available_upgrades(stripe_subscriptions_data, projects) { +export function available_upgrades(stripe_subscriptions_data, projects) { let project_id, upgrades; const available = get_total_upgrades(stripe_subscriptions_data); // start with amount available being your quota - const excess = {}; // nothing exceeds quota + const excess: Record> = {}; // nothing exceeds quota // sort projects by project_id so that excess will be well defined - const v = []; + const v: { project_id: string; upgrades: any }[] = []; for (project_id in projects) { upgrades = projects[project_id]; v.push({ project_id, upgrades }); } - v.sort((a, b) => misc.cmp(a.project_id, b.project_id)); + v.sort((a, b) => cmp(a.project_id, b.project_id)); for ({ project_id, upgrades } of v) { for (let prop in upgrades) { const curval = upgrades[prop]; @@ -83,7 +80,6 @@ function available_upgrades(stripe_subscriptions_data, projects) { } return { available, excess }; } -exports.available_upgrades = available_upgrades; // INPUT: same as above, but also a single project_id // @@ -93,13 +89,10 @@ exports.available_upgrades = available_upgrades; // {cores:2, network:1, disk_quota:2000, memory:1000} // // -function upgrade_maxes(stripe_subscriptions_data, projects, project_id) { - const { available, excess } = available_upgrades( - stripe_subscriptions_data, - projects, - ); +export function upgrade_maxes(stripe_subscriptions_data, projects, project_id) { + const { available } = available_upgrades(stripe_subscriptions_data, projects); const allocated = projects[project_id]; - const maxes = {}; + const maxes: Record = {}; for (let param in available) { const avail = available[param]; const max = PROJECT_UPGRADES.max_per_project[param]; // the maximum allowed for this param for any project @@ -108,5 +101,3 @@ function upgrade_maxes(stripe_subscriptions_data, projects, project_id) { } return maxes; } - -exports.upgrade_maxes = upgrade_maxes; diff --git a/src/scripts/add-esm-extensions.py b/src/scripts/add-esm-extensions.py new file mode 100755 index 00000000000..0b2dad81e44 --- /dev/null +++ b/src/scripts/add-esm-extensions.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +Add .js extensions to relative TypeScript imports for NodeNext/ESM compatibility. + +For each .ts/.tsx file, finds relative imports without file extensions and +adds the correct .js suffix: + + import { x } from "./foo" -> import { x } from "./foo.js" + import { x } from "../bar" -> import { x } from "../bar.js" + export { x } from "./foo" -> export { x } from "./foo.js" + import("./foo") -> import("./foo.js") + + Directory imports (./dir where dir/index.ts exists): + import { x } from "./dir" -> import { x } from "./dir/index.js" + +Imports that already have an extension (e.g. "./style.css", "./data.json") +are left unchanged. + +Usage: + python3 scripts/add-esm-extensions.py packages/util + python3 scripts/add-esm-extensions.py packages/util packages/comm + python3 scripts/add-esm-extensions.py --dry-run packages/util +""" + +import re +import sys +from pathlib import Path + +# Matches: from "./path" or from '../path' (relative, no extension) +# Captures: (quote)(path)(quote) +FROM_PATTERN = re.compile( + r"""(? bool: + """Return True if the last path segment has a file extension.""" + return bool(Path(path).suffix) + + +def resolve_esm_path(import_path: str, source_file: Path) -> str: + """ + Given a relative import path and the source file it appears in, + return the correct ESM path with .js extension. + Returns the original path unchanged if it already has an extension + or if the target cannot be resolved. + """ + if has_extension(import_path): + return import_path # already has .css, .json, .js, etc. + + source_dir = source_file.parent + target_base = source_dir / import_path + + # Check: is there a .ts or .tsx source file at this path? + for ext in (".ts", ".tsx"): + if target_base.with_suffix(ext).exists(): + return import_path + ".js" + + # Check: is it a directory with an index.ts or index.tsx? + if target_base.is_dir(): + for index_name in ("index.ts", "index.tsx"): + if (target_base / index_name).exists(): + return import_path + "/index.js" + + # Check: pre-compiled .d.ts declaration file (treat as .js at runtime) + if target_base.with_suffix(".d.ts").exists(): + return import_path + ".js" + + # Check: already a .js file (e.g. hand-written JS utility) + if target_base.with_suffix(".js").exists(): + return import_path + ".js" + + # Could not resolve — warn and leave unchanged + print( + f" WARN: cannot resolve '{import_path}' from {source_file.name}", + file=sys.stderr, + ) + return import_path + + +def fix_file(filepath: Path, dry_run: bool = False) -> bool: + """ + Fix all extensionless relative imports in a single file. + Returns True if the file was (or would be) changed. + """ + try: + content = filepath.read_text(encoding="utf-8") + except Exception as e: + print(f" ERROR reading {filepath}: {e}", file=sys.stderr) + return False + + changed = False + + def make_replacer(filepath: Path): + def replacer(m: re.Match) -> str: + nonlocal changed + prefix, q1, path, q2 = m.group(1), m.group(2), m.group(3), m.group(4) + new_path = resolve_esm_path(path, filepath) + if new_path != path: + changed = True + return f"{prefix}{q1}{new_path}{q2}" + return m.group(0) + + return replacer + + replacer = make_replacer(filepath) + new_content = FROM_PATTERN.sub(replacer, content) + new_content = DYNAMIC_PATTERN.sub(replacer, new_content) + new_content = SIDEEFFECT_PATTERN.sub(replacer, new_content) + + if changed and not dry_run: + filepath.write_text(new_content, encoding="utf-8") + + return changed + + +EXCLUDE_DIRS = {"node_modules", "dist", "dist-esm", ".git", "__pycache__"} + + +def fix_package(package_dir: Path, dry_run: bool = False) -> int: + """ + Fix all .ts/.tsx source files in a package directory. + Returns the count of files changed (or that would be changed in dry-run). + """ + count = 0 + source_files = sorted( + [ + p + for p in package_dir.rglob("*.[tj]s") + if p.suffix in (".ts", ".tsx") + and not any(part in EXCLUDE_DIRS for part in p.parts) + ] + ) + + for filepath in source_files: + if fix_file(filepath, dry_run): + rel = filepath.relative_to(package_dir) + print(f" {'[dry] ' if dry_run else ''}fixed: {rel}") + count += 1 + + return count + + +def main() -> None: + import argparse + + parser = argparse.ArgumentParser( + description="Add .js extensions to TypeScript imports for NodeNext ESM" + ) + parser.add_argument( + "packages", + nargs="+", + help="Package directories to process (e.g. packages/util)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print what would change without writing files", + ) + args = parser.parse_args() + + total = 0 + for pkg in args.packages: + pkg_path = Path(pkg) + if not pkg_path.is_dir(): + print(f"ERROR: '{pkg}' is not a directory", file=sys.stderr) + sys.exit(1) + print(f"\nProcessing {pkg_path.resolve().name}...") + count = fix_package(pkg_path, args.dry_run) + print(f" {'Would change' if args.dry_run else 'Changed'} {count} files") + total += count + + print(f"\nTotal: {total} files {'affected' if args.dry_run else 'updated'}") + + +if __name__ == "__main__": + main() diff --git a/src/update_version b/src/update_version index 8308d2f1f06..05c8bcbc28c 100755 --- a/src/update_version +++ b/src/update_version @@ -12,14 +12,14 @@ Goto account -> admin site settings -> required browser version import argparse, os, time ROOT = os.path.dirname(os.path.realpath(__file__)) -TARGET = os.path.join(ROOT, "packages/util/smc-version.js") +TARGET = os.path.join(ROOT, "packages/util/smc-version.ts") def write_version_file(): # Create version file, based on the current time now = int(time.time()) v = [] v.append("/* autogenerated by the update_version script */") - v.append("exports.version=%s;"%now) + v.append("export const version = %s;"%now) open(TARGET,'w').write('\n'.join(v) + '\n') os.chdir(os.path.join(ROOT, "packages/util")) os.system("npm run build") diff --git a/src/workspaces.py b/src/workspaces.py index 707edf20bc1..96f2c1a7219 100755 --- a/src/workspaces.py +++ b/src/workspaces.py @@ -375,11 +375,11 @@ def clean(args) -> None: v = packages(args) if args.dist_only: - folders = ['dist'] + folders = ['dist', 'dist-esm'] elif args.node_modules_only: folders = ['node_modules'] else: - folders = ['node_modules', 'dist', SUCCESSFUL_BUILD] + folders = ['node_modules', 'dist', 'dist-esm', SUCCESSFUL_BUILD] paths = [] for path in v: