@b9g/shovel
Advanced tools
Comparing version 0.1.2 to 0.1.3
#!/usr/bin/env node --experimental-vm-modules --experimental-fetch --no-warnings | ||
//#!/usr/bin/env node --experimental-vm-modules --experimental-fetch --no-warnings --inspect --inspect-brk | ||
// TODO: Figure out how to pass node flags. Is that even possible? | ||
// TODO: Squash warnings from process | ||
@@ -7,5 +8,6 @@ // https://github.com/yarnpkg/berry/blob/2cf0a8fe3e4d4bd7d4d344245d24a85a45d4c5c9/packages/yarnpkg-pnp/sources/loader/applyPatch.ts#L414-L435 | ||
import {Command} from "commander"; | ||
import whyIsNodeRunning from "why-is-node-running"; | ||
import pkg from "../package.json" assert {type: "json"}; | ||
import develop from "../src/develop.js"; | ||
const program = new Command(); | ||
@@ -18,5 +20,17 @@ program | ||
program.command("develop <file>") | ||
.description("Start a development server.") | ||
.option("-p, --port <port>", "Port to listen on", "1337") | ||
.action(develop); | ||
.action(async (file, options) => { | ||
const {develop} = await import("../src/develop.js"); | ||
await develop(file, options); | ||
}); | ||
program.command("static <file>") | ||
.description("Build a static site.") | ||
.option("--out-dir <dir>", "Output directory", "dist") | ||
.action(async (file, options) => { | ||
const {static_} = await import("../src/static.js"); | ||
await static_(file, options); | ||
}); | ||
await program.parseAsync(process.argv); |
{ | ||
"name": "@b9g/shovel", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"type": "module", | ||
"description": "Dig for treasure", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1" | ||
"test": "node --experimental-fetch $(npm bin)/uvu" | ||
}, | ||
"license": "MIT", | ||
"dependencies": { | ||
"@b9g/crank": "^0.5.3", | ||
"@emotion/css": "^11.10.6", | ||
"@pkgjs/parseargs": "^0.11.0", | ||
"@repeaterjs/repeater": "^3.0.4", | ||
"chokidar": "^3.5.3", | ||
"commander": "^10.0.0", | ||
"esbuild": "^0.17.11", | ||
@@ -28,3 +22,12 @@ "is-core-module": "^2.11.0", | ||
"access": "public" | ||
}, | ||
"devDependencies": { | ||
"@b9g/crank": "^0.5.3", | ||
"@emotion/css": "^11.10.6", | ||
"uvu": "^0.5.6", | ||
"sinon": "^15.0.2", | ||
"why-is-node-running": "^2.2.2", | ||
"commander": "^10.0.0", | ||
"fkill": "^8.1.0" | ||
} | ||
} |
@@ -5,60 +5,90 @@ import * as Path from "path"; | ||
import * as VM from "vm"; | ||
import * as ESBuild from "esbuild"; | ||
import MagicString from "magic-string"; | ||
import {Repeater} from "@repeaterjs/repeater"; | ||
import {isPathSpecifier, resolve} from "./resolve.js"; | ||
import {formatMessages} from "esbuild"; | ||
import * as Resolve from "./resolve.js"; | ||
import {createFetchServer} from "./server.js"; | ||
import {Hot, disposeHot} from "./hot.js"; | ||
import {Watcher} from "./_esbuild.js"; | ||
function watch(entry, watcherCache = new Map()) { | ||
return new Repeater(async (push, stop) => { | ||
if (watcherCache.has(entry)) { | ||
push((await watcherCache.get(entry)).result); | ||
stop(); | ||
return; | ||
//interface ModuleCacheValue { | ||
// module: VM.SourceTextModule; | ||
// dependents: Set<string>; | ||
// hot: Hot; | ||
//} | ||
const moduleCache = new Map(); | ||
function createLink(watcher) { | ||
return async function link(specifier, referencingModule) { | ||
const basedir = Path.dirname(fileURLToPath(referencingModule.identifier)); | ||
// TODO: Let’s try to use require.resolve() here. | ||
const resolved = await Resolve.resolve(specifier, basedir); | ||
if (Resolve.isPathSpecifier(specifier)) { | ||
const url = pathToFileURL(resolved).href; | ||
const result = await watcher.build(resolved); | ||
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || ""; | ||
// We don’t have to link this module because it will be linked by the | ||
// root module. | ||
if (moduleCache.has(resolved)) { | ||
moduleCache.get(resolved).dependents.add(fileURLToPath(referencingModule.identifier)); | ||
return moduleCache.get(resolved).module; | ||
} | ||
// TODO: We need to cache modules | ||
const module = new VM.SourceTextModule(code, { | ||
identifier: url, | ||
initializeImportMeta(meta) { | ||
meta.url = url; | ||
}, | ||
async importModuleDynamically(specifier, referencingModule) { | ||
const linked = await link(specifier, referencingModule); | ||
await linked.link(link); | ||
await linked.evaluate(); | ||
return linked; | ||
}, | ||
}); | ||
moduleCache.set(resolved, { | ||
module, | ||
dependents: new Set([fileURLToPath(referencingModule.identifier)]) | ||
}); | ||
return module; | ||
} else { | ||
// This is a bare module specifier so we import from node modules. | ||
const namespace = await import(resolved); | ||
const exports = Object.keys(namespace); | ||
return new VM.SyntheticModule(exports, function () { | ||
for (const key of exports) { | ||
this.setExport(key, namespace[key]); | ||
} | ||
}); | ||
} | ||
}; | ||
} | ||
let resolve; | ||
watcherCache.set(entry, { | ||
result: new Promise((r) => (resolve = r)), | ||
}); | ||
const watchPlugin = { | ||
name: "watch", | ||
setup(build) { | ||
// This is called every time a module is edited. | ||
build.onEnd(async (result) => { | ||
resolve(result); | ||
push(result); | ||
watcherCache.set(entry, {result}); | ||
}); | ||
}, | ||
}; | ||
const ctx = await ESBuild.context({ | ||
format: "esm", | ||
platform: "node", | ||
entryPoints: [entry], | ||
//bundle: true, | ||
bundle: false, | ||
metafile: true, | ||
write: false, | ||
packages: "external", | ||
sourcemap: "both", | ||
plugins: [watchPlugin], | ||
// We need this to export map files. | ||
outdir: "dist", | ||
logLevel: "silent", | ||
}); | ||
export async function develop(file, options) { | ||
file = Path.resolve(process.cwd(), file); | ||
const port = parseInt(options.port); | ||
if (Number.isNaN(port)) { | ||
throw new Error("Invalid port", options.port); | ||
} | ||
await ctx.watch(); | ||
await stop; | ||
ctx.dispose(); | ||
watcherCache.delete(entry); | ||
process.on("uncaughtException", (err) => { | ||
console.error(err); | ||
}); | ||
} | ||
export default async function develop(file, options) { | ||
const url = pathToFileURL(file).href; | ||
const port = parseInt(options.port); | ||
process.on("unhandledRejection", (err) => { | ||
console.error(err); | ||
}); | ||
process.on("SIGINT", async () => { | ||
server.close(); | ||
await watcher.dispose(); | ||
process.exit(0); | ||
}); | ||
process.on("SIGTERM", async () => { | ||
server.close(); | ||
await watcher.dispose(); | ||
process.exit(0); | ||
}); | ||
let namespace = null; | ||
let hot = null; | ||
const server = createFetchServer(async function fetcher(req) { | ||
@@ -85,102 +115,66 @@ if (typeof namespace?.default?.fetch === "function") { | ||
process.on("uncaughtException", (err) => { | ||
console.error(err); | ||
}); | ||
process.on("unhandledRejection", (err) => { | ||
console.error(err); | ||
}); | ||
const watcherCache = new Map(); | ||
for await (const result of watch(file, watcherCache)) { | ||
if (result.errors && result.errors.length) { | ||
const formatted = await ESBuild.formatMessages(result.errors, { | ||
const watcher = new Watcher(async (record, watcher) => { | ||
if (record.result.errors.length > 0) { | ||
const formatted = await formatMessages(record.result.errors, { | ||
kind: "error", | ||
color: true, | ||
}); | ||
console.error(formatted.join("\n")); | ||
continue; | ||
} else if (record.result.warnings.length > 0) { | ||
const formatted = await formatMessages(record.result.warnings, { | ||
kind: "warning", | ||
}); | ||
console.warn(formatted.join("\n")); | ||
} | ||
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text; | ||
const map = result.outputFiles.find((file) => file.path.endsWith(".map"))?.text; | ||
// TODO: Refactor by moving link and reloadRootModule to the top-level scope. | ||
async function link(specifier, referencingModule) { | ||
const basedir = Path.dirname(fileURLToPath(referencingModule.identifier)); | ||
const resolved = await resolve(specifier, basedir); | ||
if (isPathSpecifier(specifier)) { | ||
const firstResult = await new Promise(async (resolve) => { | ||
let initial = true; | ||
for await (const result of watch(resolved, watcherCache)) { | ||
if (initial) { | ||
initial = false; | ||
resolve(result); | ||
} else { | ||
// TODO: Allow import.meta.hot.accept to be called and prevent | ||
// reloading the root module. | ||
await reloadRootModule(); | ||
} | ||
// TODO: Rather than reloading the root module, we should bubble changes | ||
// from dependencies to dependents according to import.meta.hot | ||
if (!record.initial) { | ||
const queue = [record.entry]; | ||
while (queue.length > 0) { | ||
const entry = queue.shift(); | ||
const dependents = moduleCache.get(entry)?.dependents; | ||
if (dependents) { | ||
for (const dependent of dependents) { | ||
queue.push(dependent); | ||
} | ||
}); | ||
} | ||
const code = firstResult.outputFiles.find((file) => file.path.endsWith(".js"))?.text; | ||
const depURL = pathToFileURL(resolved).href; | ||
return new VM.SourceTextModule(code || "", { | ||
identifier: depURL, | ||
initializeImportMeta(meta) { | ||
meta.url = depURL; | ||
meta.hot = hot; | ||
}, | ||
async importModuleDynamically(specifier, module) { | ||
const linked = await link(specifier, module); | ||
await linked.link(link); | ||
await linked.evaluate(); | ||
return linked; | ||
}, | ||
}); | ||
moduleCache.delete(entry); | ||
} | ||
const child = await import(resolved); | ||
const exports = Object.keys(child); | ||
return new VM.SyntheticModule(exports, function () { | ||
for (const key of exports) { | ||
this.setExport(key, child[key]); | ||
} | ||
}); | ||
const rootResult = await watcher.build(file); | ||
await reload(rootResult); | ||
} | ||
}); | ||
async function reloadRootModule() { | ||
if (hot) { | ||
disposeHot(hot); | ||
} | ||
const link = createLink(watcher); | ||
async function reload(result) { | ||
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || ""; | ||
const url = pathToFileURL(file).href; | ||
const module = new VM.SourceTextModule(code, { | ||
identifier: url, | ||
initializeImportMeta(meta) { | ||
meta.url = url; | ||
}, | ||
async importModuleDynamically(specifier, referencingModule) { | ||
const linked = await link(specifier, referencingModule); | ||
await linked.link(link); | ||
await linked.evaluate(); | ||
return linked; | ||
}, | ||
}); | ||
const module = new VM.SourceTextModule(code, { | ||
identifier: url, | ||
initializeImportMeta(meta) { | ||
meta.url = url; | ||
meta.hot = hot; | ||
}, | ||
async importModuleDynamically(specifier, module) { | ||
const linked = await link(specifier, module); | ||
await linked.link(link); | ||
await linked.evaluate(); | ||
return linked; | ||
}, | ||
}); | ||
try { | ||
await module.link(link); | ||
await module.evaluate(); | ||
} catch (err) { | ||
console.error(err); | ||
return; | ||
} | ||
try { | ||
await module.link(link); | ||
await module.evaluate(); | ||
namespace = module.namespace; | ||
hot = new Hot(); | ||
console.info("rebuilt:", url); | ||
} catch (err) { | ||
console.error(err); | ||
namespace = null; | ||
} | ||
} | ||
await reloadRootModule(); | ||
} | ||
const result = await watcher.build(file); | ||
await reload(result); | ||
return server; | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
26004
5
22
834
2
7
7
8
- Removed@b9g/crank@^0.5.3
- Removed@emotion/css@^11.10.6
- Removed@pkgjs/parseargs@^0.11.0
- Removed@repeaterjs/repeater@^3.0.4
- Removedchokidar@^3.5.3
- Removedcommander@^10.0.0
- Removed@b9g/crank@0.5.7(transitive)
- Removed@babel/code-frame@7.26.2(transitive)
- Removed@babel/generator@7.26.2(transitive)
- Removed@babel/helper-module-imports@7.25.9(transitive)
- Removed@babel/helper-string-parser@7.25.9(transitive)
- Removed@babel/helper-validator-identifier@7.25.9(transitive)
- Removed@babel/parser@7.26.2(transitive)
- Removed@babel/runtime@7.26.0(transitive)
- Removed@babel/template@7.25.9(transitive)
- Removed@babel/traverse@7.25.9(transitive)
- Removed@babel/types@7.26.0(transitive)
- Removed@emotion/babel-plugin@11.13.5(transitive)
- Removed@emotion/cache@11.13.5(transitive)
- Removed@emotion/css@11.13.5(transitive)
- Removed@emotion/hash@0.9.2(transitive)
- Removed@emotion/memoize@0.9.0(transitive)
- Removed@emotion/serialize@1.3.3(transitive)
- Removed@emotion/sheet@1.4.0(transitive)
- Removed@emotion/unitless@0.10.0(transitive)
- Removed@emotion/utils@1.4.2(transitive)
- Removed@emotion/weak-memoize@0.4.0(transitive)
- Removed@jridgewell/gen-mapping@0.3.5(transitive)
- Removed@jridgewell/resolve-uri@3.1.2(transitive)
- Removed@jridgewell/set-array@1.2.1(transitive)
- Removed@jridgewell/trace-mapping@0.3.25(transitive)
- Removed@pkgjs/parseargs@0.11.0(transitive)
- Removed@repeaterjs/repeater@3.0.6(transitive)
- Removed@types/parse-json@4.0.2(transitive)
- Removedanymatch@3.1.3(transitive)
- Removedbabel-plugin-macros@3.1.0(transitive)
- Removedbinary-extensions@2.3.0(transitive)
- Removedbraces@3.0.3(transitive)
- Removedcallsites@3.1.0(transitive)
- Removedchokidar@3.6.0(transitive)
- Removedcommander@10.0.1(transitive)
- Removedconvert-source-map@1.9.0(transitive)
- Removedcosmiconfig@7.1.0(transitive)
- Removedcsstype@3.1.3(transitive)
- Removeddebug@4.3.7(transitive)
- Removederror-ex@1.3.2(transitive)
- Removedescape-string-regexp@4.0.0(transitive)
- Removedfill-range@7.1.1(transitive)
- Removedfind-root@1.1.0(transitive)
- Removedfsevents@2.3.3(transitive)
- Removedglob-parent@5.1.2(transitive)
- Removedglobals@11.12.0(transitive)
- Removedimport-fresh@3.3.0(transitive)
- Removedis-arrayish@0.2.1(transitive)
- Removedis-binary-path@2.1.0(transitive)
- Removedis-extglob@2.1.1(transitive)
- Removedis-glob@4.0.3(transitive)
- Removedis-number@7.0.0(transitive)
- Removedjs-tokens@4.0.0(transitive)
- Removedjsesc@3.0.2(transitive)
- Removedjson-parse-even-better-errors@2.3.1(transitive)
- Removedlines-and-columns@1.2.4(transitive)
- Removedms@2.1.3(transitive)
- Removednormalize-path@3.0.0(transitive)
- Removedparent-module@1.0.1(transitive)
- Removedparse-json@5.2.0(transitive)
- Removedpath-parse@1.0.7(transitive)
- Removedpath-type@4.0.0(transitive)
- Removedpicocolors@1.1.1(transitive)
- Removedpicomatch@2.3.1(transitive)
- Removedreaddirp@3.6.0(transitive)
- Removedregenerator-runtime@0.14.1(transitive)
- Removedresolve@1.22.8(transitive)
- Removedresolve-from@4.0.0(transitive)
- Removedsource-map@0.5.7(transitive)
- Removedstylis@4.2.0(transitive)
- Removedsupports-preserve-symlinks-flag@1.0.0(transitive)
- Removedto-regex-range@5.0.1(transitive)
- Removedyaml@1.10.2(transitive)