@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 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
- ChatGPT-4:
3
+ Shovel is a CLI for building web applications.
5
4
 
6
- The name "shovel" for a CLI development server, static site generator, or full stack API can have various connotations:
5
+ ## Commands
7
6
 
8
- 1. Functionality: A shovel is a versatile tool used for digging, lifting, and moving materials, which can symbolize the tool's ability to handle different tasks in a development environment. In this context, the name could imply that the tool is flexible, adaptable, and capable of handling a variety of jobs in the development process.
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
- It's important to note that the actual connotations and perception of the name "shovel" will depend on the specific tool's features, performance, and the user's individual experiences with the software.
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
- // TODO: Figure out how to pass node flags. Is that even possible?
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" assert {type: "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")
@@ -14,4 +14,3 @@ export default {
14
14
  return ["/", "/hello", "/world"];
15
15
  },
16
16
  };
17
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b9g/shovel",
3
- "version": "0.1.6",
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 $(npm bin)/uvu"
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
- export class Watcher {
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
- this.cache = new Map();
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.cache.get(entry);
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
- build(entry) {
48
- if (this.cache.has(entry)) {
49
- return this.cache.get(entry).result;
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 = createESBuildContext(entry, [this.plugin]);
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
- for (const {ctx} of this.cache.values()) {
71
- await ctx.dispose();
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 webRequestFromNode(req) {
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 callNodeResponse(res, webRes) {
37
- const headers = {};
38
- webRes.headers.forEach((value, key) => {
39
- headers[key] = value;
40
- });
41
- res.writeHead(webRes.status, headers);
42
- // TODO: stream the body
43
- res.end(await webRes.text());
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
- const server = createServer(async (req, res) => {
48
- const webRes = await handler(webRequestFromNode(req));
49
- callNodeResponse(res, webRes);
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 * as FS from "fs/promises";
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
- // TODO: We need to cache modules
34
- const module = new VM.SourceTextModule(code, {
35
- identifier: url,
36
- initializeImportMeta(meta) {
37
- meta.url = url;
38
- },
39
- async importModuleDynamically(specifier, referencingModule) {
40
- const linked = await link(specifier, referencingModule);
41
- await linked.link(link);
42
- await linked.evaluate();
43
- return linked;
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
- moduleCache.set(resolved, {
48
- module,
49
- dependents: new Set([fileURLToPath(referencingModule.identifier)])
50
- });
51
- return module;
52
- } else {
53
- // This is a bare module specifier so we import from node modules.
54
- const namespace = await import(resolved);
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(file, options) {
66
- file = Path.resolve(process.cwd(), file);
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 watcher.dispose();
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 watcher.dispose();
61
+ await observer.dispose();
89
62
  process.exit(0);
90
63
  });
91
64
 
92
- let namespace = null;
65
+ let module = null;
93
66
  const server = createFetchServer(async function fetcher(req) {
94
- if (typeof namespace?.default?.fetch === "function") {
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("listening on port:", port);
83
+ console.info("Listening on port:", port);
112
84
  });
113
85
 
114
- const watcher = new Watcher(async (record, watcher) => {
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
- queue.push(dependent);
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 watcher.build(file);
144
- await reload(rootResult);
120
+ const rootResult = await observer.build(entry);
121
+ module = await executeBuildResult(rootResult, entry, link);
145
122
  }
146
123
  });
147
124
 
148
- const link = createLink(watcher);
149
- async function reload(result) {
150
- const code = result.outputFiles.find((file) => file.path.endsWith(".js"))?.text || "";
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 {fileURLToPath, pathToFileURL} from "url";
3
+ import {pathToFileURL} from "url";
4
4
  import * as VM from "vm";
5
5
  import {formatMessages} from "esbuild";
6
- import * as Resolve from "./resolve.js";
7
- import {Watcher} from "./_esbuild.js";
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 watcher.dispose();
22
+ await observer.dispose();
68
23
  process.exit(0);
69
24
  });
70
25
 
71
26
  process.on("SIGTERM", async () => {
72
- await watcher.dispose();
27
+ await observer.dispose();
73
28
  process.exit(0);
74
29
  });
75
30
 
76
- const watcher = new Watcher(async (record, watcher) => {
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 result = await watcher.build(file);
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(createLink(watcher));
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 url = pathToFileURL(path);
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 shovel = ChildProcess.spawn(
24
- "shovel",
25
- ["develop", "./fixtures/server-hello.ts", "--port", PORT],
26
- );
27
- const serverIsRunning = async () => {
28
- try {
29
- const response = await fetch(`http://localhost:${PORT}`);
30
- return await response.text();
31
- } catch (err) {
32
- return false;
33
- }
34
- };
35
-
36
- let isRunning = false;
37
- let tries = 0;
38
- while (!isRunning) {
39
- if (tries > 30) {
40
- throw new Error("Server never started");
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 = await serverIsRunning();
44
- if (!isRunning) {
45
- await new Promise((resolve) => setTimeout(resolve, 100));
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 shovel = ChildProcess.spawn(
54
- "shovel",
55
- ["develop", "./fixtures/server-hello.ts", "--port", PORT],
56
- {stdio: "inherit"},
57
- );
58
-
59
- const serverIsRunning = async () => {
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
- isRunning = await serverIsRunning();
76
- if (!isRunning) {
77
- await new Promise((resolve) => setTimeout(resolve, 100));
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
- Assert.equal(isRunning, "<marquee>Hello world</marquee>");
79
+ Assert.equal(isRunning, "<marquee>Hello world</marquee>");
82
80
 
83
- const serverHelloContents = await FS.readFile(
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
- const response = await fetch(`http://localhost:${PORT}`);
97
- const text = await response.text();
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
- Assert.equal(text, "<marquee>Goodbye world</marquee>");
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 FS.writeFile("./fixtures/server-hello.ts", serverHelloContents);
102
+ await fkill(`:${PORT}`);
102
103
  }
103
104
  });
104
105
 
105
106
  test("restarts on change to dependency", async () => {
106
- const shovel = ChildProcess.spawn(
107
- "shovel",
108
- ["develop", "./fixtures/server-dependent.ts", "--port", PORT],
109
- {stdio: "inherit"},
110
- );
111
-
112
- const serverIsRunning = async () => {
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
- isRunning = await serverIsRunning();
129
- if (!isRunning) {
130
- await new Promise((resolve) => setTimeout(resolve, 100));
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
- try {
141
- await FS.copyFile(
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
- await new Promise((resolve) => setTimeout(resolve, 1000));
147
- const response = await fetch(`http://localhost:${PORT}`);
148
- const text = await response.text();
143
+ try {
144
+ await FS.copyFile(
145
+ "./fixtures/server-dependency-goodbye.ts",
146
+ "./fixtures/server-dependency-hello.ts",
147
+ );
149
148
 
150
- Assert.equal("<marquee>Goodbye from dependency-hello.ts</marquee>", text);
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 FS.writeFile(
153
- "./fixtures/server-dependency-hello.ts",
154
- serverDependencyContents,
155
- );
161
+ await fkill(`:${PORT}`);
156
162
  }
157
163
  });
158
164