@b9g/shovel 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -9
- package/bin/shovel.js +6 -3
- package/fixtures/static.js +0 -1
- package/package.json +5 -2
- package/src/_esbuild.js +49 -31
- package/src/{resolve.js → _resolve.js} +2 -3
- package/src/{server.js → _server.js} +24 -15
- package/src/_vm.js +59 -0
- package/src/develop.js +50 -100
- package/src/static.js +18 -62
- package/test/develop.js +123 -117
package/README.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# Shovel
|
|
2
|
-
Me: What are the connotations of a CLI development server / static site generator / soon to be full stack API being named "shovel"?
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Shovel is a CLI for building web applications.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## Commands
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
1. Simplicity: Shovels are simple and straightforward tools, which can imply that the named software is designed to be easy to use and understand. The name could convey that the tool aims to provide a user-friendly experience, with a focus on minimizing complexity and maximizing ease of use for developers.
|
|
10
|
-
1. Robustness: Shovels are known for their durability and ability to withstand harsh conditions. This connotation could suggest that the software is designed to be reliable and robust, capable of handling heavy workloads and providing consistent performance.
|
|
11
|
-
1. Groundbreaking: A shovel is often used to break ground for new construction projects, symbolizing new beginnings and progress. In the context of a development tool, this could represent the innovative nature of the software, as well as its potential to contribute to the growth and evolution of web development.
|
|
12
|
-
1. Humor: The name "shovel" could also be perceived as a tongue-in-cheek reference to the software, potentially implying a light-hearted, playful approach to development. This can be appealing to developers who appreciate humor or unconventional naming conventions in the tools they use.
|
|
7
|
+
- `shovel develop`
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
- `shovel build`
|
|
10
|
+
|
|
11
|
+
- `shovel static`
|
|
12
|
+
Build a static website.
|
package/bin/shovel.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node --experimental-vm-modules --experimental-fetch --no-warnings
|
|
2
|
+
// TODO: Figure out how to allow debug mode via flags.
|
|
2
3
|
//#!/usr/bin/env node --experimental-vm-modules --experimental-fetch --no-warnings --inspect --inspect-brk
|
|
3
|
-
//
|
|
4
|
+
//
|
|
4
5
|
// TODO: Squash warnings from process
|
|
6
|
+
// This code might help
|
|
5
7
|
// https://github.com/yarnpkg/berry/blob/2cf0a8fe3e4d4bd7d4d344245d24a85a45d4c5c9/packages/yarnpkg-pnp/sources/loader/applyPatch.ts#L414-L435
|
|
6
|
-
|
|
7
8
|
import {Command} from "commander";
|
|
8
9
|
|
|
9
|
-
import pkg from "../package.json"
|
|
10
|
+
import pkg from "../package.json" with {type: "json"};
|
|
11
|
+
|
|
12
|
+
process.title = "shovel";
|
|
10
13
|
const program = new Command();
|
|
11
14
|
program
|
|
12
15
|
.name("shovel")
|
package/fixtures/static.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@b9g/shovel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Dig for treasure",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
"shovel": "bin/shovel.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node --experimental-fetch
|
|
11
|
+
"test": "node --experimental-fetch node_modules/.bin/uvu"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@b9g/shovel": "^0.1.6",
|
|
14
15
|
"commander": "^10.0.0",
|
|
15
16
|
"esbuild": "^0.17.11",
|
|
16
17
|
"is-core-module": "^2.11.0",
|
|
@@ -21,8 +22,10 @@
|
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@b9g/crank": "^0.5.3",
|
|
23
24
|
"@emotion/css": "^11.10.6",
|
|
25
|
+
"@flydotio/dockerfile": "^0.5.0",
|
|
24
26
|
"fkill": "^8.1.0",
|
|
25
27
|
"sinon": "^15.0.2",
|
|
28
|
+
"typescript": "^5.3.3",
|
|
26
29
|
"uvu": "^0.5.6"
|
|
27
30
|
},
|
|
28
31
|
"publishConfig": {
|
package/src/_esbuild.js
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
import * as ESBuild from "esbuild";
|
|
2
|
-
function createESBuildContext(entry, plugins) {
|
|
3
|
-
return ESBuild.context({
|
|
4
|
-
entryPoints: [entry],
|
|
5
|
-
plugins,
|
|
6
|
-
format: "esm",
|
|
7
|
-
platform: "node",
|
|
8
|
-
bundle: false,
|
|
9
|
-
metafile: true,
|
|
10
|
-
write: false,
|
|
11
|
-
packages: "external",
|
|
12
|
-
sourcemap: "both",
|
|
13
|
-
// We need this to export map files.
|
|
14
|
-
outdir: "dist",
|
|
15
|
-
logLevel: "silent",
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
2
|
|
|
19
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} WatcherEntry
|
|
5
|
+
* @property {string} entry
|
|
6
|
+
* @property {ESBuild.BuildResult} result
|
|
7
|
+
* @property {initial} boolean
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class BuildObserver {
|
|
11
|
+
/**
|
|
12
|
+
* @param {(record: WatcherEntry, observer: BuildObserver) => any} callback
|
|
13
|
+
* callback
|
|
14
|
+
*/
|
|
20
15
|
constructor(callback) {
|
|
21
|
-
|
|
16
|
+
/** @type {Map<string, WatcherEntry>} */
|
|
17
|
+
this._cache = new Map();
|
|
22
18
|
this.callback = callback;
|
|
23
19
|
this.plugin = {
|
|
24
20
|
name: "watcher",
|
|
25
21
|
setup: (build) => {
|
|
22
|
+
/**
|
|
23
|
+
* @param {ESBuild.BuildResult} result
|
|
24
|
+
*/
|
|
26
25
|
build.onEnd(async (result) => {
|
|
27
26
|
// TODO: errors in this callback seem to be swallowed
|
|
28
27
|
const entry = build.initialOptions.entryPoints[0];
|
|
29
|
-
const cacheValue = this.
|
|
28
|
+
const cacheValue = this._cache.get(entry);
|
|
30
29
|
const initial = cacheValue.resolve != null;
|
|
31
30
|
if (cacheValue.resolve) {
|
|
32
31
|
cacheValue.resolve(result);
|
|
@@ -44,12 +43,31 @@ export class Watcher {
|
|
|
44
43
|
};
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
/**
|
|
47
|
+
* @param {string} entry
|
|
48
|
+
* @param {ESBuild.BuildOptions} options
|
|
49
|
+
* @returns {Promise<ESBuild.BuildResult>}
|
|
50
|
+
*/
|
|
51
|
+
async build(entry) {
|
|
52
|
+
if (this._cache.has(entry)) {
|
|
53
|
+
return this._cache.get(entry).result;
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
const ctxP =
|
|
56
|
+
const ctxP = ESBuild.context({
|
|
57
|
+
entryPoints: [entry],
|
|
58
|
+
plugins: [this.plugin],
|
|
59
|
+
format: "esm",
|
|
60
|
+
platform: "node",
|
|
61
|
+
bundle: false,
|
|
62
|
+
metafile: true,
|
|
63
|
+
write: false,
|
|
64
|
+
packages: "external",
|
|
65
|
+
sourcemap: "both",
|
|
66
|
+
// We need this to export map files.
|
|
67
|
+
outdir: "dist",
|
|
68
|
+
logLevel: "silent",
|
|
69
|
+
});
|
|
70
|
+
|
|
53
71
|
let resolve = null;
|
|
54
72
|
const cacheValue = {
|
|
55
73
|
entry,
|
|
@@ -57,18 +75,18 @@ export class Watcher {
|
|
|
57
75
|
result: new Promise((r) => (resolve = r)),
|
|
58
76
|
resolve,
|
|
59
77
|
};
|
|
60
|
-
this.cache.set(entry, cacheValue);
|
|
61
|
-
ctxP.then((ctx) => {
|
|
62
|
-
ctx.watch();
|
|
63
|
-
cacheValue.ctx = ctx;
|
|
64
|
-
});
|
|
65
78
|
|
|
79
|
+
this._cache.set(entry, cacheValue);
|
|
80
|
+
const ctx = await ctxP;
|
|
81
|
+
ctx.watch();
|
|
82
|
+
cacheValue.ctx = ctx;
|
|
66
83
|
return cacheValue.result;
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
async dispose() {
|
|
70
|
-
|
|
71
|
-
await ctx
|
|
72
|
-
|
|
87
|
+
await Promise.all([...this._cache.values()].map(async (value) => {
|
|
88
|
+
await value.ctx;
|
|
89
|
+
return value.ctx.dispose();
|
|
90
|
+
}));
|
|
73
91
|
}
|
|
74
92
|
}
|
|
@@ -104,7 +104,7 @@ async function loadAsFile(x) {
|
|
|
104
104
|
|
|
105
105
|
async function loadAsDirectory(x) {
|
|
106
106
|
let pkgdir;
|
|
107
|
-
try {
|
|
107
|
+
try {
|
|
108
108
|
pkgdir = await realpath(x);
|
|
109
109
|
} catch (err) {
|
|
110
110
|
throw err;
|
|
@@ -171,7 +171,6 @@ export async function resolve(specifier, basedir) {
|
|
|
171
171
|
if (isPathSpecifier(specifier)) {
|
|
172
172
|
let specifier1 = Path.resolve(basedir, specifier);
|
|
173
173
|
|
|
174
|
-
// TODO: What is this for?
|
|
175
174
|
if (specifier === '.' || specifier === '..' || specifier.slice(-1) === '/') {
|
|
176
175
|
specifier1 += '/';
|
|
177
176
|
}
|
|
@@ -188,7 +187,7 @@ export async function resolve(specifier, basedir) {
|
|
|
188
187
|
}
|
|
189
188
|
|
|
190
189
|
result = await loadAsFile(specifier1);
|
|
191
|
-
}
|
|
190
|
+
}
|
|
192
191
|
|
|
193
192
|
if (!result) {
|
|
194
193
|
throw new Error(`Cannot resolve ${specifier} from ${basedir}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {createServer} from "http";
|
|
2
|
+
|
|
2
3
|
function readableStreamFromMessage(req) {
|
|
3
4
|
return new ReadableStream({
|
|
4
5
|
start(controller) {
|
|
@@ -17,7 +18,7 @@ function readableStreamFromMessage(req) {
|
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function
|
|
21
|
+
function createRequestFromNode(req) {
|
|
21
22
|
const url = new URL(req.url || "/", "http://" + req.headers.host);
|
|
22
23
|
const headers = new Headers();
|
|
23
24
|
for (const key in req.headers) {
|
|
@@ -28,26 +29,34 @@ function webRequestFromNode(req) {
|
|
|
28
29
|
|
|
29
30
|
return new Request(url, {
|
|
30
31
|
method: req.method,
|
|
31
|
-
headers,
|
|
32
32
|
body: req.method === "GET" || req.method === "HEAD" ? undefined : readableStreamFromMessage(req),
|
|
33
|
+
headers,
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
async function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
async function writeNodeResponse(res, webRes) {
|
|
38
|
+
const headers = {};
|
|
39
|
+
webRes.headers.forEach((value, key) => {
|
|
40
|
+
headers[key] = value;
|
|
41
|
+
});
|
|
42
|
+
res.writeHead(webRes.status, headers);
|
|
43
|
+
|
|
44
|
+
const reader = webRes.body.getReader();
|
|
45
|
+
// Why isn’t the reader async iterable???
|
|
46
|
+
while (true) {
|
|
47
|
+
const {value, done} = await reader.read();
|
|
48
|
+
if (done) {
|
|
49
|
+
res.end();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
res.write(value);
|
|
54
|
+
}
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export function createFetchServer(handler) {
|
|
47
|
-
|
|
48
|
-
const webRes = await handler(
|
|
49
|
-
|
|
58
|
+
return createServer(async (req, res) => {
|
|
59
|
+
const webRes = await handler(createRequestFromNode(req));
|
|
60
|
+
await writeNodeResponse(res, webRes);
|
|
50
61
|
});
|
|
51
|
-
|
|
52
|
-
return server;
|
|
53
62
|
}
|
package/src/_vm.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as Path from "path";
|
|
2
|
+
import * as VM from "vm";
|
|
3
|
+
|
|
4
|
+
import {fileURLToPath, pathToFileURL} from "url";
|
|
5
|
+
import * as Resolve from "./_resolve.js";
|
|
6
|
+
|
|
7
|
+
export function createLink(buildObserver, moduleCache = new Map()) {
|
|
8
|
+
return async function link(specifier, referencingModule) {
|
|
9
|
+
const basedir = Path.dirname(fileURLToPath(referencingModule.identifier));
|
|
10
|
+
// TODO: Let’s try to use require.resolve() here.
|
|
11
|
+
const resolvedSpecifier = await Resolve.resolve(specifier, basedir);
|
|
12
|
+
if (Resolve.isPathSpecifier(specifier)) {
|
|
13
|
+
const result = await buildObserver.build(resolvedSpecifier);
|
|
14
|
+
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || "";
|
|
15
|
+
|
|
16
|
+
// We don’t have to link this module because it will be linked by the
|
|
17
|
+
// root module.
|
|
18
|
+
if (moduleCache.has(resolvedSpecifier)) {
|
|
19
|
+
const {module, dependents} = moduleCache.get(resolvedSpecifier);
|
|
20
|
+
dependents.add(fileURLToPath(referencingModule.identifier).href);
|
|
21
|
+
return module;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const url = pathToFileURL(resolvedSpecifier).href;
|
|
25
|
+
// TODO: We need to cache modules
|
|
26
|
+
const module = new VM.SourceTextModule(code, {
|
|
27
|
+
identifier: url,
|
|
28
|
+
initializeImportMeta(meta) {
|
|
29
|
+
meta.url = url;
|
|
30
|
+
},
|
|
31
|
+
async importModuleDynamically(specifier, referencingModule) {
|
|
32
|
+
const linkedModule = await link(specifier, referencingModule);
|
|
33
|
+
await linkedModule.link(link);
|
|
34
|
+
await linkedModule.evaluate();
|
|
35
|
+
return linkedModule;
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
moduleCache.set(resolvedSpecifier, {
|
|
40
|
+
module,
|
|
41
|
+
dependents: new Set([fileURLToPath(referencingModule.identifier)])
|
|
42
|
+
});
|
|
43
|
+
return module;
|
|
44
|
+
} else {
|
|
45
|
+
// This is a bare module specifier so we import from node modules.
|
|
46
|
+
if (resolvedSpecifier == null) {
|
|
47
|
+
throw new Error(`Could not resolve ${specifier}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const namespace = await import(resolvedSpecifier);
|
|
51
|
+
const exports = Object.keys(namespace);
|
|
52
|
+
return new VM.SyntheticModule(exports, function () {
|
|
53
|
+
for (const key of exports) {
|
|
54
|
+
this.setExport(key, namespace[key]);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/develop.js
CHANGED
|
@@ -1,69 +1,42 @@
|
|
|
1
1
|
import * as Path from "path";
|
|
2
|
-
import
|
|
3
|
-
import {fileURLToPath, pathToFileURL} from "url";
|
|
2
|
+
import {pathToFileURL} from "url";
|
|
4
3
|
import * as VM from "vm";
|
|
5
4
|
import {formatMessages} from "esbuild";
|
|
6
|
-
import * as Resolve from "./resolve.js";
|
|
7
|
-
import {createFetchServer} from "./server.js";
|
|
8
|
-
import {Watcher} from "./_esbuild.js";
|
|
9
|
-
|
|
10
|
-
//interface ModuleCacheValue {
|
|
11
|
-
// module: VM.SourceTextModule;
|
|
12
|
-
// dependents: Set<string>;
|
|
13
|
-
// hot: Hot;
|
|
14
|
-
//}
|
|
15
|
-
const moduleCache = new Map();
|
|
16
|
-
function createLink(watcher) {
|
|
17
|
-
return async function link(specifier, referencingModule) {
|
|
18
|
-
const basedir = Path.dirname(fileURLToPath(referencingModule.identifier));
|
|
19
|
-
// TODO: Let’s try to use require.resolve() here.
|
|
20
|
-
const resolved = await Resolve.resolve(specifier, basedir);
|
|
21
|
-
if (Resolve.isPathSpecifier(specifier)) {
|
|
22
|
-
const url = pathToFileURL(resolved).href;
|
|
23
|
-
const result = await watcher.build(resolved);
|
|
24
|
-
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || "";
|
|
25
|
-
|
|
26
|
-
// We don’t have to link this module because it will be linked by the
|
|
27
|
-
// root module.
|
|
28
|
-
if (moduleCache.has(resolved)) {
|
|
29
|
-
moduleCache.get(resolved).dependents.add(fileURLToPath(referencingModule.identifier));
|
|
30
|
-
return moduleCache.get(resolved).module;
|
|
31
|
-
}
|
|
32
5
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
6
|
+
import {BuildObserver} from "./_esbuild.js";
|
|
7
|
+
import {createLink} from "./_vm.js";
|
|
8
|
+
import {createFetchServer} from "./_server.js";
|
|
9
|
+
|
|
10
|
+
async function executeBuildResult(result, entry, link) {
|
|
11
|
+
const javascript = result.outputFiles.find((file) =>
|
|
12
|
+
file.path.endsWith(".js")
|
|
13
|
+
)?.text || "";
|
|
14
|
+
const url = pathToFileURL(entry).href;
|
|
15
|
+
const module = new VM.SourceTextModule(javascript, {
|
|
16
|
+
identifier: url,
|
|
17
|
+
initializeImportMeta(meta) {
|
|
18
|
+
meta.url = url;
|
|
19
|
+
},
|
|
20
|
+
async importModuleDynamically(specifier, referencingModule) {
|
|
21
|
+
const linked = await link(specifier, referencingModule);
|
|
22
|
+
await linked.link(link);
|
|
23
|
+
await linked.evaluate();
|
|
24
|
+
return linked;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
46
27
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const exports = Object.keys(namespace);
|
|
56
|
-
return new VM.SyntheticModule(exports, function () {
|
|
57
|
-
for (const key of exports) {
|
|
58
|
-
this.setExport(key, namespace[key]);
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
};
|
|
28
|
+
try {
|
|
29
|
+
await module.link(link);
|
|
30
|
+
await module.evaluate();
|
|
31
|
+
return module;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(err);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
63
36
|
}
|
|
64
37
|
|
|
65
|
-
export async function develop(
|
|
66
|
-
|
|
38
|
+
export async function develop(entry, options) {
|
|
39
|
+
entry = Path.resolve(process.cwd(), entry);
|
|
67
40
|
const port = parseInt(options.port);
|
|
68
41
|
if (Number.isNaN(port)) {
|
|
69
42
|
throw new Error("Invalid port", options.port);
|
|
@@ -79,23 +52,22 @@ export async function develop(file, options) {
|
|
|
79
52
|
|
|
80
53
|
process.on("SIGINT", async () => {
|
|
81
54
|
server.close();
|
|
82
|
-
await
|
|
55
|
+
await observer.dispose();
|
|
83
56
|
process.exit(0);
|
|
84
57
|
});
|
|
85
58
|
|
|
86
59
|
process.on("SIGTERM", async () => {
|
|
87
60
|
server.close();
|
|
88
|
-
await
|
|
61
|
+
await observer.dispose();
|
|
89
62
|
process.exit(0);
|
|
90
63
|
});
|
|
91
64
|
|
|
92
|
-
let
|
|
65
|
+
let module = null;
|
|
93
66
|
const server = createFetchServer(async function fetcher(req) {
|
|
94
|
-
if (typeof namespace
|
|
67
|
+
if (typeof module?.namespace.default?.fetch === "function") {
|
|
95
68
|
try {
|
|
96
|
-
return await namespace?.default?.fetch(req);
|
|
69
|
+
return await module?.namespace?.default?.fetch(req);
|
|
97
70
|
} catch (err) {
|
|
98
|
-
console.error(err);
|
|
99
71
|
return new Response(err.stack, {
|
|
100
72
|
status: 500,
|
|
101
73
|
});
|
|
@@ -108,10 +80,11 @@ export async function develop(file, options) {
|
|
|
108
80
|
});
|
|
109
81
|
|
|
110
82
|
server.listen(port, () => {
|
|
111
|
-
console.info("
|
|
83
|
+
console.info("Listening on port:", port);
|
|
112
84
|
});
|
|
113
85
|
|
|
114
|
-
const
|
|
86
|
+
const moduleCache = new Map();
|
|
87
|
+
const observer = new BuildObserver(async (record, observer) => {
|
|
115
88
|
if (record.result.errors.length > 0) {
|
|
116
89
|
const formatted = await formatMessages(record.result.errors, {
|
|
117
90
|
kind: "error",
|
|
@@ -127,52 +100,29 @@ export async function develop(file, options) {
|
|
|
127
100
|
// TODO: Rather than reloading the root module, we should bubble changes
|
|
128
101
|
// from dependencies to dependents according to import.meta.hot
|
|
129
102
|
if (!record.initial) {
|
|
103
|
+
const seen = new Set([record.entry]);
|
|
130
104
|
const queue = [record.entry];
|
|
131
105
|
while (queue.length > 0) {
|
|
132
106
|
const entry = queue.shift();
|
|
133
107
|
const dependents = moduleCache.get(entry)?.dependents;
|
|
134
108
|
if (dependents) {
|
|
135
109
|
for (const dependent of dependents) {
|
|
136
|
-
|
|
110
|
+
if (!seen.has(dependent)) {
|
|
111
|
+
seen.add(dependent);
|
|
112
|
+
queue.push(dependent);
|
|
113
|
+
}
|
|
137
114
|
}
|
|
138
115
|
}
|
|
139
116
|
|
|
140
117
|
moduleCache.delete(entry);
|
|
141
118
|
}
|
|
142
119
|
|
|
143
|
-
const rootResult = await
|
|
144
|
-
await
|
|
120
|
+
const rootResult = await observer.build(entry);
|
|
121
|
+
module = await executeBuildResult(rootResult, entry, link);
|
|
145
122
|
}
|
|
146
123
|
});
|
|
147
124
|
|
|
148
|
-
const link = createLink(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const url = pathToFileURL(file).href;
|
|
152
|
-
const module = new VM.SourceTextModule(code, {
|
|
153
|
-
identifier: url,
|
|
154
|
-
initializeImportMeta(meta) {
|
|
155
|
-
meta.url = url;
|
|
156
|
-
},
|
|
157
|
-
async importModuleDynamically(specifier, referencingModule) {
|
|
158
|
-
const linked = await link(specifier, referencingModule);
|
|
159
|
-
await linked.link(link);
|
|
160
|
-
await linked.evaluate();
|
|
161
|
-
return linked;
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
await module.link(link);
|
|
167
|
-
await module.evaluate();
|
|
168
|
-
namespace = module.namespace;
|
|
169
|
-
} catch (err) {
|
|
170
|
-
console.error(err);
|
|
171
|
-
namespace = null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const result = await watcher.build(file);
|
|
176
|
-
await reload(result);
|
|
177
|
-
return server;
|
|
125
|
+
const link = createLink(observer, moduleCache);
|
|
126
|
+
const result = await observer.build(entry);
|
|
127
|
+
module = await executeBuildResult(result, entry, link);
|
|
178
128
|
}
|
package/src/static.js
CHANGED
|
@@ -1,79 +1,34 @@
|
|
|
1
1
|
import * as Path from "path";
|
|
2
2
|
import * as FS from "fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import {pathToFileURL} from "url";
|
|
4
4
|
import * as VM from "vm";
|
|
5
5
|
import {formatMessages} from "esbuild";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
// TODO: The static workflow is run once so we don’t need to observe files.
|
|
8
|
+
import {BuildObserver} from "./_esbuild.js";
|
|
9
|
+
import {createLink} from "./_vm.js";
|
|
10
|
+
|
|
11
|
+
// TODO: This code is duplicated in ./develop.js so it should be moved to a
|
|
12
|
+
// module-specific file.
|
|
8
13
|
|
|
9
14
|
//interface ModuleCacheValue {
|
|
10
15
|
// module: VM.SourceTextModule;
|
|
11
16
|
// dependents: Set<string>;
|
|
12
17
|
// hot: Hot;
|
|
13
18
|
//}
|
|
14
|
-
const moduleCache = new Map();
|
|
15
|
-
function createLink(watcher) {
|
|
16
|
-
return async function link(specifier, referencingModule) {
|
|
17
|
-
const basedir = Path.dirname(fileURLToPath(referencingModule.identifier));
|
|
18
|
-
// TODO: Let’s try to use require.resolve() here.
|
|
19
|
-
const resolved = await Resolve.resolve(specifier, basedir);
|
|
20
|
-
if (Resolve.isPathSpecifier(specifier)) {
|
|
21
|
-
const url = pathToFileURL(resolved).href;
|
|
22
|
-
const result = await watcher.build(resolved);
|
|
23
|
-
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || "";
|
|
24
|
-
|
|
25
|
-
// We don’t have to link this module because it will be linked by the
|
|
26
|
-
// root module.
|
|
27
|
-
if (moduleCache.has(resolved)) {
|
|
28
|
-
moduleCache.get(resolved).dependents.add(fileURLToPath(referencingModule.identifier));
|
|
29
|
-
return moduleCache.get(resolved).module;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// TODO: We need to cache modules
|
|
33
|
-
const module = new VM.SourceTextModule(code, {
|
|
34
|
-
identifier: url,
|
|
35
|
-
initializeImportMeta(meta) {
|
|
36
|
-
meta.url = url;
|
|
37
|
-
},
|
|
38
|
-
async importModuleDynamically(specifier, referencingModule) {
|
|
39
|
-
const linked = await link(specifier, referencingModule);
|
|
40
|
-
await linked.link(link);
|
|
41
|
-
await linked.evaluate();
|
|
42
|
-
return linked;
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
moduleCache.set(resolved, {
|
|
47
|
-
module,
|
|
48
|
-
dependents: new Set([fileURLToPath(referencingModule.identifier)])
|
|
49
|
-
});
|
|
50
|
-
return module;
|
|
51
|
-
} else {
|
|
52
|
-
// This is a bare module specifier so we import from node modules.
|
|
53
|
-
const namespace = await import(resolved);
|
|
54
|
-
const exports = Object.keys(namespace);
|
|
55
|
-
return new VM.SyntheticModule(exports, function () {
|
|
56
|
-
for (const key of exports) {
|
|
57
|
-
this.setExport(key, namespace[key]);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
19
|
export async function static_(file, options) {
|
|
65
20
|
file = Path.resolve(process.cwd(), file);
|
|
66
21
|
process.on("SIGINT", async () => {
|
|
67
|
-
await
|
|
22
|
+
await observer.dispose();
|
|
68
23
|
process.exit(0);
|
|
69
24
|
});
|
|
70
25
|
|
|
71
26
|
process.on("SIGTERM", async () => {
|
|
72
|
-
await
|
|
27
|
+
await observer.dispose();
|
|
73
28
|
process.exit(0);
|
|
74
29
|
});
|
|
75
30
|
|
|
76
|
-
const
|
|
31
|
+
const observer = new BuildObserver(async (record) => {
|
|
77
32
|
if (record.result.errors.length > 0) {
|
|
78
33
|
const formatted = await formatMessages(record.result.errors, {
|
|
79
34
|
kind: "error",
|
|
@@ -85,11 +40,11 @@ export async function static_(file, options) {
|
|
|
85
40
|
kind: "warning",
|
|
86
41
|
});
|
|
87
42
|
console.warn(formatted.join("\n"));
|
|
88
|
-
process.exit(0);
|
|
89
43
|
}
|
|
90
44
|
});
|
|
91
45
|
|
|
92
|
-
const
|
|
46
|
+
const link = createLink(observer);
|
|
47
|
+
const result = await observer.build(file);
|
|
93
48
|
const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || "";
|
|
94
49
|
const url = pathToFileURL(file).href;
|
|
95
50
|
const module = new VM.SourceTextModule(code, {
|
|
@@ -98,6 +53,7 @@ export async function static_(file, options) {
|
|
|
98
53
|
meta.url = url;
|
|
99
54
|
},
|
|
100
55
|
async importModuleDynamically(specifier, referencingModule) {
|
|
56
|
+
// TODO: link is not defined so I dunno how this works.
|
|
101
57
|
const linked = await link(specifier, referencingModule);
|
|
102
58
|
await linked.link(link);
|
|
103
59
|
await linked.evaluate();
|
|
@@ -105,19 +61,19 @@ export async function static_(file, options) {
|
|
|
105
61
|
},
|
|
106
62
|
});
|
|
107
63
|
|
|
108
|
-
await module.link(
|
|
64
|
+
await module.link(link);
|
|
109
65
|
await module.evaluate();
|
|
110
66
|
const namespace = module.namespace;
|
|
111
67
|
const dist = Path.resolve(process.cwd(), options.outDir);
|
|
112
68
|
const paths = await namespace.default?.staticPaths?.(dist);
|
|
113
69
|
if (paths) {
|
|
114
70
|
for await (const path of paths) {
|
|
115
|
-
const
|
|
116
|
-
const req = new Request(url.href);
|
|
71
|
+
const req = new Request(pathToFileURL(path).href);
|
|
117
72
|
const res = await namespace.default?.fetch?.(req);
|
|
118
73
|
const body = await res.text();
|
|
74
|
+
// TODO: we need an alternative to /index.html style builds.
|
|
119
75
|
const file = Path.resolve(dist, path.replace(/^\//, ""), "index.html");
|
|
120
|
-
console.info(`Writing ${file}`);
|
|
76
|
+
console.info(`Writing: ${file}`);
|
|
121
77
|
// ensure directory exists
|
|
122
78
|
await FS.mkdir(Path.dirname(file), {recursive: true});
|
|
123
79
|
await FS.writeFile(file, body);
|
package/test/develop.js
CHANGED
|
@@ -3,156 +3,162 @@ import {suite} from "uvu";
|
|
|
3
3
|
import * as Assert from "uvu/assert";
|
|
4
4
|
import * as Sinon from "sinon";
|
|
5
5
|
import * as ChildProcess from "child_process";
|
|
6
|
-
import whyIsNodeRunning from "why-is-node-running";
|
|
6
|
+
//import whyIsNodeRunning from "why-is-node-running";
|
|
7
7
|
import fkill from "fkill";
|
|
8
8
|
|
|
9
9
|
const test = suite("develop");
|
|
10
10
|
|
|
11
|
-
const PORT = "1339";
|
|
12
|
-
test.after.each(async () => {
|
|
13
|
-
try {
|
|
14
|
-
await fkill(`:${PORT}`);
|
|
15
|
-
} catch (err) {
|
|
16
|
-
// uvu test hooks seem to fail silently...
|
|
17
|
-
console.error(err);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
11
|
// TODO: wait for server to be ready rather than retrying
|
|
22
12
|
test("basic", async () => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
13
|
+
const PORT = 13307;
|
|
14
|
+
try {
|
|
15
|
+
const shovel = ChildProcess.spawn(
|
|
16
|
+
"./bin/shovel.js",
|
|
17
|
+
["develop", "./fixtures/server-hello.ts", "--port", PORT],
|
|
18
|
+
{stdio: "inherit"},
|
|
19
|
+
);
|
|
20
|
+
const serverIsRunning = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(`http://localhost:${PORT}`);
|
|
23
|
+
return await response.text();
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let isRunning = false;
|
|
30
|
+
let tries = 0;
|
|
31
|
+
while (!isRunning) {
|
|
32
|
+
if (tries > 5) {
|
|
33
|
+
throw new Error("Server never started");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isRunning = await serverIsRunning();
|
|
37
|
+
if (!isRunning) {
|
|
38
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
39
|
+
}
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
isRunning
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
42
|
+
Assert.equal(isRunning, "<marquee>Hello world</marquee>");
|
|
43
|
+
} finally {
|
|
44
|
+
await fkill(`:${PORT}`);
|
|
47
45
|
}
|
|
48
|
-
|
|
49
|
-
Assert.equal(isRunning, "<marquee>Hello world</marquee>");
|
|
50
46
|
});
|
|
51
47
|
|
|
52
48
|
test("restarts on change to root", async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const response = await fetch(`http://localhost:${PORT}`);
|
|
62
|
-
return await response.text();
|
|
63
|
-
} catch (err) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
let isRunning = false;
|
|
69
|
-
let tries = 0;
|
|
70
|
-
while (!isRunning) {
|
|
71
|
-
if (tries > 30) {
|
|
72
|
-
throw new Error("Server never started");
|
|
73
|
-
}
|
|
49
|
+
const PORT = 13308;
|
|
50
|
+
try {
|
|
51
|
+
const shovel = ChildProcess.spawn(
|
|
52
|
+
"./bin/shovel.js",
|
|
53
|
+
["develop", "./fixtures/server-hello.ts", "--port", PORT],
|
|
54
|
+
{stdio: "inherit"},
|
|
55
|
+
);
|
|
74
56
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
57
|
+
const serverIsRunning = async () => {
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(`http://localhost:${PORT}`);
|
|
60
|
+
return await response.text();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let isRunning = false;
|
|
67
|
+
let tries = 0;
|
|
68
|
+
while (!isRunning) {
|
|
69
|
+
if (tries > 30) {
|
|
70
|
+
throw new Error("Server never started");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
isRunning = await serverIsRunning();
|
|
74
|
+
if (!isRunning) {
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
76
|
+
}
|
|
78
77
|
}
|
|
79
|
-
}
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
Assert.equal(isRunning, "<marquee>Hello world</marquee>");
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
"./fixtures/server-hello.ts",
|
|
85
|
-
"utf8",
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
await FS.copyFile(
|
|
90
|
-
"./fixtures/server-goodbye.ts",
|
|
81
|
+
const serverHelloContents = await FS.readFile(
|
|
91
82
|
"./fixtures/server-hello.ts",
|
|
83
|
+
"utf8",
|
|
92
84
|
);
|
|
93
|
-
// TODO: wait for server to restart
|
|
94
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
95
85
|
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
try {
|
|
87
|
+
await FS.copyFile(
|
|
88
|
+
"./fixtures/server-goodbye.ts",
|
|
89
|
+
"./fixtures/server-hello.ts",
|
|
90
|
+
);
|
|
91
|
+
// TODO: wait for server to restart
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
93
|
+
|
|
94
|
+
const response = await fetch(`http://localhost:${PORT}`);
|
|
95
|
+
const text = await response.text();
|
|
98
96
|
|
|
99
|
-
|
|
97
|
+
Assert.equal(text, "<marquee>Goodbye world</marquee>");
|
|
98
|
+
} finally {
|
|
99
|
+
await FS.writeFile("./fixtures/server-hello.ts", serverHelloContents);
|
|
100
|
+
}
|
|
100
101
|
} finally {
|
|
101
|
-
await
|
|
102
|
+
await fkill(`:${PORT}`);
|
|
102
103
|
}
|
|
103
104
|
});
|
|
104
105
|
|
|
105
106
|
test("restarts on change to dependency", async () => {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const response = await fetch(`http://localhost:${PORT}`);
|
|
115
|
-
return await response.text();
|
|
116
|
-
} catch (err) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
let isRunning = false;
|
|
122
|
-
let tries = 0;
|
|
123
|
-
while (!isRunning) {
|
|
124
|
-
if (tries > 30) {
|
|
125
|
-
throw new Error("Server never started");
|
|
126
|
-
}
|
|
107
|
+
const PORT = 13309;
|
|
108
|
+
try {
|
|
109
|
+
const shovel = ChildProcess.spawn(
|
|
110
|
+
"./bin/shovel.js",
|
|
111
|
+
["develop", "./fixtures/server-dependent.ts", "--port", PORT],
|
|
112
|
+
{stdio: "inherit"},
|
|
113
|
+
);
|
|
127
114
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
115
|
+
const serverIsRunning = async () => {
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(`http://localhost:${PORT}`);
|
|
118
|
+
return await response.text();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let isRunning = false;
|
|
125
|
+
let tries = 0;
|
|
126
|
+
while (!isRunning) {
|
|
127
|
+
if (tries > 30) {
|
|
128
|
+
throw new Error("Server never started");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isRunning = await serverIsRunning();
|
|
132
|
+
if (!isRunning) {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
134
|
+
}
|
|
131
135
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
Assert.equal(isRunning, "<marquee>Hello from dependency-hello.ts</marquee>");
|
|
135
|
-
const serverDependencyContents = await FS.readFile(
|
|
136
|
-
"./fixtures/server-dependency-hello.ts",
|
|
137
|
-
"utf8",
|
|
138
|
-
);
|
|
139
136
|
|
|
140
|
-
|
|
141
|
-
await FS.
|
|
142
|
-
"./fixtures/server-dependency-goodbye.ts",
|
|
137
|
+
Assert.equal(isRunning, "<marquee>Hello from dependency-hello.ts</marquee>");
|
|
138
|
+
const serverDependencyContents = await FS.readFile(
|
|
143
139
|
"./fixtures/server-dependency-hello.ts",
|
|
140
|
+
"utf8",
|
|
144
141
|
);
|
|
145
142
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
try {
|
|
144
|
+
await FS.copyFile(
|
|
145
|
+
"./fixtures/server-dependency-goodbye.ts",
|
|
146
|
+
"./fixtures/server-dependency-hello.ts",
|
|
147
|
+
);
|
|
149
148
|
|
|
150
|
-
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
150
|
+
const response = await fetch(`http://localhost:${PORT}`);
|
|
151
|
+
const text = await response.text();
|
|
152
|
+
|
|
153
|
+
Assert.equal("<marquee>Goodbye from dependency-hello.ts</marquee>", text);
|
|
154
|
+
} finally {
|
|
155
|
+
await FS.writeFile(
|
|
156
|
+
"./fixtures/server-dependency-hello.ts",
|
|
157
|
+
serverDependencyContents,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
151
160
|
} finally {
|
|
152
|
-
await
|
|
153
|
-
"./fixtures/server-dependency-hello.ts",
|
|
154
|
-
serverDependencyContents,
|
|
155
|
-
);
|
|
161
|
+
await fkill(`:${PORT}`);
|
|
156
162
|
}
|
|
157
163
|
});
|
|
158
164
|
|