diff --git a/.jshintrc b/.jshintrc index fbfceab..6459544 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,17 +1,11 @@ - - - - - - { + "module": true, "browser": true, "node": true, "esversion": 11, "curly": true, "sub": true, - "bitwise": true, "eqeqeq": true, "forin": true, @@ -23,7 +17,6 @@ "plusplus": true, "undef": true, "unused": "vars", - "strict": true, "maxdepth": 4, "maxstatements": 100, "maxcomplexity": 20 diff --git a/README.md b/README.md index 64c6488..1556c36 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,24 @@ Turn on transpile-free type hinting for your vanilla JS projects #JSWithTypes npm init ``` 2. Turn on type linting 💪 + ```sh # Create a properly configured `jsconfig.json` - npx jswt init + npx -p jswt@2.x -- jswt init + + # Install @types/node, if needed + npm install --save '@types/node' ``` + 3. Profit! Works with **VS Code** out-of-the-box, and [Vim + ale](https://webinstall.dev/vim-essentials). +## CommonJS vs ESM + +Use v1 for CommonJS, or v2 for ESM. + ## Watch the Presentation! [![JS with Types conference title slide](https://jswithtypes.com/assets/utahjs-conf-2022-jswt-title-yt.png)](https://jswithtypes.com/) @@ -49,13 +58,14 @@ Your project will look something like this: npx -p typescript@5.x -- \ tsc --init \ --allowJs --alwaysStrict --checkJs \ - --moduleResolution node \ - --noEmit --noImplicitAny --target es2022 \ + --module nextnode --moduleResolution nextnode \ + --noEmit --noImplicitAny --target es2024 \ --typeRoots './typings,./node_modules/@types' ``` 2. Adds the following keys: ```txt - "include": ["*.js", "bin/**/*.js", "lib/**/*.js", "src/**/*.js"]` + "paths": { "foo": ["./foo.js"], "foo/*": ["./*"] }, + "include": ["*.js", "bin/**/*.js", "lib/**/*.js", "src/**/*.js"]`, "exclude": ["node_modules"] ``` 3. Renames `tsconfig.json` to `jsconfig.json` \ diff --git a/bin/_walk.js b/bin/_walk.js index e8b2447..f75ba04 100644 --- a/bin/_walk.js +++ b/bin/_walk.js @@ -1,10 +1,10 @@ -"use strict"; +import Fs from "node:fs/promises"; +import Path from "node:path"; -const Fs = require("node:fs/promises"); -const Path = require("node:path"); +let Walk = {}; +Walk.skipDir = new Error("skip this directory"); -const skipDir = new Error("skip this directory"); -const _withFileTypes = { withFileTypes: true }; +let _withFileTypes = { withFileTypes: true }; /** @typedef {import('fs').Dirent} Dirent */ @@ -24,7 +24,7 @@ const _withFileTypes = { withFileTypes: true }; * @param {Dirent} [_dirent] * @returns {Promise} */ -const walk = async (pathname, walkFunc, _dirent) => { +Walk.walk = async function (pathname, walkFunc, _dirent) { let err; // special case of the very first run @@ -41,7 +41,7 @@ const walk = async (pathname, walkFunc, _dirent) => { // run the user-supplied function and either skip, bail, or continue let cont = await walkFunc(err, pathname, _dirent).catch(function (err) { - if (skipDir === err) { + if (Walk.skipDir === err) { return false; } throw err; @@ -65,11 +65,9 @@ const walk = async (pathname, walkFunc, _dirent) => { ); for (let dirent of dirents) { - await walk(Path.join(pathname, dirent.name), walkFunc, dirent); + let path = Path.join(pathname, dirent.name); + await Walk.walk(path, walkFunc, dirent); } }; -module.exports = { - walk, - skipDir, -}; +export default Walk; diff --git a/bin/jswt-init.js b/bin/jswt-init.js index 5e7deac..2530b74 100755 --- a/bin/jswt-init.js +++ b/bin/jswt-init.js @@ -1,16 +1,19 @@ #!/usr/bin/env node -"use strict"; -//require("dotenv").config({ path: ".env" }); -//require("dotenv").config({ path: ".env.secret" }); +//import Dotenv from "dotenv"; +//Dotenv.config({ path: ".env" }); +//Dotenv.config({ path: ".env.secret" }); const PKG_NAME = "package.json"; const TSC_NAME = "tsconfig.json"; const JSC_NAME = "jsconfig.json"; -var Fs = require("node:fs").promises; -var Path = require("node:path"); -var spawn = require("node:child_process").spawn; +import Fs from "node:fs/promises"; +import Path from "node:path"; +import ChildProcess from "node:child_process"; + +let modulePath = import.meta.url.slice("file://".length); +let moduleDir = Path.dirname(modulePath); async function main() { /* jshint maxcomplexity: 25 */ @@ -31,12 +34,6 @@ async function main() { pkgName = pkgName.slice(index); } - let jsconfig = await readJsConfig(); - if (!jsconfig) { - let tsconfigTxt = await createTsConfig(); - jsconfig = await createJsConfig(pkg, tsconfigTxt); - } - // await Fs.mkdir("./docs", { recursive: true }); // let fh = await Fs.open("./docs/.gitkeep", "a"); // await fh.close(); @@ -81,16 +78,27 @@ async function main() { await initFile( "./index.js", [ - `"use strict";`, "// auto-generated by `jswt reexport`", "// DO NOT EDIT", "", - `module.exports = require("${prefix}/${pkgName}.js");`, + `import ${pkgName} from "${prefix}/${pkgName}.js";`, + "", + `export default ${pkgName};`, "", ].join("\n"), ); } + let jsconfig = await readJsConfig(); + if (!jsconfig) { + let tsconfigTxt = await createTsConfig(); + jsconfig = await createJsConfig( + pkg, + tsconfigTxt, + `${prefix}/${pkgName}.js`, + ); + } + let eslintFile = await whichEslint(".", pkg); let initJshint = !flags.noJshint && !eslintFile; if (initJshint) { @@ -98,6 +106,7 @@ async function main() { "./.jshintrc", [ `{`, + ` "module": true,`, ` "browser": true,`, ` "node": true,`, ` "esversion": 11,`, @@ -115,7 +124,6 @@ async function main() { ` "plusplus": true,`, ` "undef": true,`, ` "unused": "vars",`, - ` "strict": true,`, ` "maxdepth": 4,`, ` "maxstatements": 100,`, ` "maxcomplexity": 20`, @@ -145,7 +153,7 @@ async function main() { let hasTestRunnerScript = script.includes(testRunnerScript); if (hasTestRunnerScript) { await Fs.mkdir("./tests", { recursive: true }); - let testRunnerPath = Path.join(__dirname, "../tests/index.js"); + let testRunnerPath = Path.join(moduleDir, "../tests/index.js"); let testRunnerText = await Fs.readFile(testRunnerPath, "utf8"); await initFile("./tests/index.js", testRunnerText); } @@ -169,7 +177,7 @@ async function main() { let ghaDir = `.github/workflows`; let ghaFile = `.github/workflows/node.js.yml`; await Fs.mkdir(`./${ghaDir}`, { recursive: true }); - let ghActionPath = Path.join(__dirname, `../${ghaFile}`); + let ghActionPath = Path.join(moduleDir, `../${ghaFile}`); let ghActionText = await Fs.readFile(ghActionPath, "utf8"); await initFile(`./${ghaFile}`, ghActionText); } @@ -214,12 +222,12 @@ async function main() { await initFile( mainPath, [ - `"use strict";`, - "", - `let ${mainName} = module.exports;`, + `let ${mainName} = {};`, "", `${mainName}.answer = 42;`, "", + `export default ${mainName};`, + "", ].join("\n"), ); } @@ -229,6 +237,7 @@ async function main() { if (mainIsIndex) { let allArgs = ["pkg", "set", `main=${mainPath}`]; await exec("npm", allArgs); + await sortAndWritePackageJson(); } } @@ -278,6 +287,7 @@ async function main() { `files[]=tests/*.js`, ]; await exec("npm", allArgs); + await sortAndWritePackageJson(); } } @@ -290,10 +300,57 @@ async function main() { void (await upsertNpmScript( "prepublish", "reexport-types", - "npx -p jswt@1.x -- reexport", + "npx -p jswt@2.x -- reexport", "reexport", )); + { + let result = await exec("npm", ["pkg", "get", "exports"]); + let curScript = result.stdout.trim(); + if ("{}" === curScript) { + let allArgs = [ + "pkg", + "set", + "type=module", + `exports[.]=${mainPath}`, + `exports[./*]=./*`, + ]; + await exec("npm", allArgs); + await sortAndWritePackageJson(); + } + } + + { + let result = await exec("npm", ["pkg", "get", "imports"]); + let curScript = result.stdout.trim(); + if ("{}" === curScript) { + let allArgs = [ + "pkg", + "set", + "type=module", + `imports[${pkg.name}]=${mainPath}`, + ]; + await exec("npm", allArgs); + await sortAndWritePackageJson(); + + await initFile( + "./importmap.html", + [ + ``, + "", + ].join("\n"), + ); + } + } + let jsconfigTxt = JSON.stringify(jsconfig, null, 2); // for stderr / debug output console.error(`./jsconfig.json:`); @@ -505,13 +562,14 @@ async function createTsConfig() { "--allowJs", "--alwaysStrict", "--checkJs", + "--module", + "nodenext", "--moduleResolution", - "node", + "nodenext", "--noEmit", "--noImplicitAny", "--target", - // can't be 'esnext' due to some resolution issues - version || "es2022", + version || "es2022", // can't be 'esnext' due to some resolution issues "--typeRoots", "./typings,./node_modules/@types", ]; @@ -556,7 +614,7 @@ async function getLatest20xx() { return version; } version = m[0]; - //console.log("version", version); + //console.log("DEBUG version", version); } return version; }); @@ -564,10 +622,12 @@ async function getLatest20xx() { /** * @param {Object} pkg + * @param {String} pkg.name * @param {String} tsconfigTxt + * @param {String} mainPath * @returns */ -async function createJsConfig(pkg, tsconfigTxt) { +async function createJsConfig(pkg, tsconfigTxt, mainPath) { if (!tsconfigTxt.includes(`"include":`)) { let includables = [ "*.js", @@ -584,6 +644,18 @@ async function createJsConfig(pkg, tsconfigTxt) { tsconfigTxt = tsconfigTxt.replace(/\n}[\s\n]*$/m, `${includeLine}\n}`); } + { + let lines = [ + ` "${pkg.name}": ["${mainPath}"]`, + ` "${pkg.name}/*": ["./*"]`, + ]; + let str = lines.join(`,\n`); + tsconfigTxt = tsconfigTxt.replace( + `// "paths": {},`, + `"paths": {\n${str}\n }, `, + ); + } + if (!tsconfigTxt.includes(`"exclude":`)) { let excludeLine = `,\n "exclude": ["node_modules"]`; tsconfigTxt = tsconfigTxt.replace(/\n}[\s\n]*$/m, `${excludeLine}\n}`); @@ -665,6 +737,54 @@ async function initFile(fileName, initValue) { await Fs.writeFile(fileName, initValue, "utf8"); } +async function sortAndWritePackageJson() { + let pkg = await readPackageJson(); + + /** @type {Object.} */ + let newPkg = {}; + let pkgKeys = Object.keys(pkg); + + let orderedKeys = [ + "name", + "version", + "description", + "main", + "files", + "type", + "imports", + "exports", + "scripts", + ]; + for (let key of orderedKeys) { + let hasKey = remove(pkgKeys, key); + if (hasKey) { + newPkg[key] = pkg[key]; + } + } + for (let key of pkgKeys) { + newPkg[key] = pkg[key]; + } + + let pkgJson = JSON.stringify(newPkg, null, 2); + await Fs.writeFile(PKG_NAME, pkgJson, "utf8"); +} + +/** + * @param {Array} arr + * @param {String} str + * @returns {String?} + */ +function remove(arr, str) { + let index = arr.indexOf(str); + if (index > -1) { + let removed = arr.splice(index, 1); + let el = removed[0] || null; + return el; + } + + return null; +} + /** * @param {String} metaKey - ex: lint, fmt, start * @param {String} scriptKey - ex: jshint, prettier @@ -723,8 +843,8 @@ async function upsertNpmScript( * @param {Array} args */ async function exec(exe, args) { - return new Promise(function (resolve, reject) { - let cmd = spawn(exe, args); + return await new Promise(function (resolve, reject) { + let cmd = ChildProcess.spawn(exe, args); /** @type {Array} */ let stdout = []; diff --git a/bin/jswt-reexport.js b/bin/jswt-reexport.js index a2aaff4..fa8fd3b 100755 --- a/bin/jswt-reexport.js +++ b/bin/jswt-reexport.js @@ -1,17 +1,17 @@ #!/usr/bin/env node -"use strict"; -//require("dotenv").config({ path: ".env" }); -//require("dotenv").config({ path: ".env.secret" }); +//import Dotenv from "dotenv"; +//Dotenv.config({ path: ".env" }); +//Dotenv.config({ path: ".env.secret" }); -var Fs = require("node:fs/promises"); -var Path = require("node:path"); -var Walk = require("./_walk.js"); +import Fs from "node:fs/promises"; +import Path from "node:path"; +import Walk from "./_walk.js"; -var LOADABLE = [".js", ".cjs", ".mjs"]; +let LOADABLE = [".js", ".cjs", ".mjs"]; // TODO make configurable -var IGNORABLE = [ +let IGNORABLE = [ "index.js", "types.js", "build", @@ -19,8 +19,8 @@ var IGNORABLE = [ "node_modules", "tmp", ]; -var IGNORABLE_PATTERNS = /\.min\./; -var IGNORABLE_TYPES = [".bak", ".git", ".tmp"]; +let IGNORABLE_PATTERNS = /\.min\./; +let IGNORABLE_TYPES = [".bak", ".git", ".tmp"]; /** * @typedef Typedef @@ -112,15 +112,15 @@ async function writeIndexJs(pkgName, indexLines) { }); let lines = [ - `"use strict";`, "// auto-generated by `jswt reexport`", "// DO NOT EDIT", "", - `module.exports = require("${prefix}/${pkgName}.js");`, + `import "${prefix}/${pkgName}.js";`, "", "// these typedef reexports will be available to dependent packages", "/**", - ].concat(indexLines, [" */", ""]); + ]; + lines = lines.concat(indexLines, [" */", ""]); await Fs.readFile("./index.js", "utf8") .then(function (txt) { diff --git a/bin/jswt.js b/bin/jswt.js index c01e116..37a9464 100755 --- a/bin/jswt.js +++ b/bin/jswt.js @@ -1,14 +1,19 @@ #!/usr/bin/env node -"use strict"; -//require("dotenv").config({ path: ".env" }); -//require("dotenv").config({ path: ".env.secret" }); +//import Dotenv from "dotenv"; +//Dotenv.config({ path: ".env" }); +//Dotenv.config({ path: ".env.secret" }); -//@ts-ignore -let pkg = require("../package.json"); +import Fs from "node:fs/promises"; +import FsSync from "node:fs"; +import Path from "node:path"; -let Fs = require("node:fs/promises"); -let Path = require("node:path"); +let modulePath = import.meta.url.slice("file://".length); +let moduleDir = Path.dirname(modulePath); + +let pkgPath = Path.join(moduleDir, "../package.json"); +let pkgText = FsSync.readFileSync(pkgPath, "utf8"); +let pkg = JSON.parse(pkgText); function showVersion() { console.info(`jswt v${pkg.version}`); @@ -56,7 +61,7 @@ async function main() { } } - let subcmdPath = Path.join(__dirname, `jswt-${subcmd}.js`); + let subcmdPath = Path.join(moduleDir, `jswt-${subcmd}.js`); let notExists = await Fs.access(subcmdPath).catch(Object); if (notExists) { console.error(`error: '${subcmd}' is not a valid subcommand`); @@ -65,7 +70,7 @@ async function main() { return; } - await require(`./jswt-${subcmd}.js`); + await import(`./jswt-${subcmd}.js`); } main().catch(function (err) { diff --git a/index.js b/index.js index e9b35d0..9aeafe4 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,7 @@ -"use strict"; // auto-generated by `jswt reexport` // DO NOT EDIT -module.exports = require("./lib/jswt.js"); +import "./lib/jswt.js"; // these typedef reexports will be available to dependent packages /** diff --git a/jsconfig.json b/jsconfig.json index 755c597..2ec03d7 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -25,9 +25,9 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, + "module": "nodenext" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, + "moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ diff --git a/lib/jswt.js b/lib/jswt.js index d0ed354..f667d2b 100644 --- a/lib/jswt.js +++ b/lib/jswt.js @@ -1,2 +1,5 @@ +let JSWT = {}; + // intentionally left blank -module.exports = {}; + +export default JSWT; diff --git a/package-lock.json b/package-lock.json index 045ac83..68c57d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "jswt", - "version": "1.7.0", - "lockfileVersion": 2, + "version": "2.0.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "jswt", - "version": "1.7.0", + "version": "2.0.0", "license": "SEE LICENSE IN LICENSE", "bin": { "init": "bin/jswt-init.js", @@ -34,22 +34,5 @@ "dev": true, "license": "MIT" } - }, - "dependencies": { - "@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", - "dev": true, - "requires": { - "undici-types": "~6.20.0" - } - }, - "undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", - "dev": true - } } } diff --git a/package.json b/package.json index 5b69da1..fbb4232 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "jswt", - "version": "1.7.0", + "version": "2.0.0", "description": "Turn on transpile-free type hinting for your vanilla JS projects #JSWithTypes", "main": "index.js", + "type": "module", "files": [ "index.js", "bin/*.js", @@ -47,5 +48,12 @@ "homepage": "https://github.com/BeyondCodeBootcamp/jswt#readme", "devDependencies": { "@types/node": "^22.10.2" + }, + "exports": { + ".": "./lib/jswt.js", + "./*": "./*" + }, + "imports": { + "jswt": "./lib/jswt.js" } } diff --git a/tests/index.js b/tests/index.js index 5ad7b26..7773362 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,13 +1,14 @@ -"use strict"; +import ChildProcess from "child_process"; +import Fs from "node:fs/promises"; +import Path from "node:path"; -let ChildProcess = require("child_process"); -let Fs = require("node:fs/promises"); -let Path = require("node:path"); +let modulePath = import.meta.url.slice("file://".length); +let moduleDir = Path.dirname(modulePath); async function main() { console.info("TAP version 13"); - let dirents = await Fs.readdir(__dirname, { withFileTypes: true }); + let dirents = await Fs.readdir(moduleDir, { withFileTypes: true }); let failures = 0; let count = 0; @@ -17,7 +18,7 @@ async function main() { } count += 1; - let direntPath = Path.join(__dirname, dirent.name); + let direntPath = Path.join(moduleDir, dirent.name); let relPath = Path.relative(".", direntPath); let success = await handleEach(count, relPath); diff --git a/tests/init.js b/tests/init.js index 235315d..5efbf31 100644 --- a/tests/init.js +++ b/tests/init.js @@ -1,4 +1,3 @@ #!/usr/bin/env node -"use strict"; -require("../bin/jswt-init.js"); +import "../bin/jswt-init.js";