@webstir-io/webstir 0.1.2 → 0.1.4

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.
@@ -313,15 +313,6 @@ function ensureBackdrop(root: HTMLElement): void {
313
313
  return;
314
314
  }
315
315
 
316
- const legacy = root.querySelector<HTMLElement>('.webstir-search__backdrop');
317
- if (legacy) {
318
- legacy.classList.add('ws-drawer-backdrop');
319
- legacy.classList.remove('webstir-search__backdrop');
320
- legacy.setAttribute('data-webstir-search-close', '');
321
- legacy.setAttribute('aria-hidden', 'true');
322
- return;
323
- }
324
-
325
316
  const created = document.createElement('div');
326
317
  created.className = 'ws-drawer-backdrop';
327
318
  created.setAttribute('data-webstir-search-close', '');
@@ -1,4 +1,6 @@
1
1
  import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
 
3
5
  import {
4
6
  createDefaultBunBackendBootstrap,
@@ -54,19 +56,8 @@ export async function start(): Promise<void> {
54
56
  );
55
57
  }
56
58
 
57
- const isMain = (() => {
58
- try {
59
- const argv1 = process.argv?.[1];
60
- if (!argv1) return false;
61
- const here = new URL(import.meta.url);
62
- const run = new URL(`file://${argv1}`);
63
- return here.pathname === run.pathname;
64
- } catch {
65
- return false;
66
- }
67
- })();
68
-
69
- if (isMain) {
59
+ const entrypointPath = process.argv[1];
60
+ if (entrypointPath && path.resolve(entrypointPath) === fileURLToPath(import.meta.url)) {
70
61
  start().catch((err) => {
71
62
  console.error(err);
72
63
  process.exitCode = 1;
@@ -1,4 +1,6 @@
1
1
  import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
 
3
5
  import {
4
6
  createDefaultBunBackendBootstrap,
@@ -54,19 +56,8 @@ export async function start(): Promise<void> {
54
56
  );
55
57
  }
56
58
 
57
- const isMain = (() => {
58
- try {
59
- const argv1 = process.argv?.[1];
60
- if (!argv1) return false;
61
- const here = new URL(import.meta.url);
62
- const run = new URL(`file://${argv1}`);
63
- return here.pathname === run.pathname;
64
- } catch {
65
- return false;
66
- }
67
- })();
68
-
69
- if (isMain) {
59
+ const entrypointPath = process.argv[1];
60
+ if (entrypointPath && path.resolve(entrypointPath) === fileURLToPath(import.meta.url)) {
70
61
  start().catch((err) => {
71
62
  console.error(err);
72
63
  process.exitCode = 1;
@@ -4,8 +4,8 @@ import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import type { RouteHandlerResult } from '@webstir-io/webstir-backend/runtime/bun';
6
6
 
7
- const ROOT_PATH = '/';
8
- const DEMO_PATH = '/demo/progressive-enhancement';
7
+ const ROOT_PATH = '/api';
8
+ const DEMO_PATH = '/api/demo/progressive-enhancement';
9
9
  const FRAGMENT_TARGET = 'greeting-preview';
10
10
  const SESSION_PANEL_TARGET = 'session-panel';
11
11
  const SESSION_COOKIE_NAME = 'webstir_demo_session';
@@ -2,7 +2,7 @@ import { assert, test } from '@webstir-io/webstir-testing';
2
2
 
3
3
  test('progressive enhancement demo page renders a form shell', async () => {
4
4
  const ctx = requireBackendTestContext();
5
- const response = await ctx.request('/demo/progressive-enhancement');
5
+ const response = await ctx.request('/api/demo/progressive-enhancement');
6
6
  const html = await response.text();
7
7
 
8
8
  assert.equal(response.status, 200);
@@ -23,7 +23,7 @@ test('progressive enhancement demo page renders a form shell', async () => {
23
23
 
24
24
  test('native form submissions redirect back to the document route', async () => {
25
25
  const ctx = requireBackendTestContext();
26
- const response = await ctx.request('/demo/progressive-enhancement', {
26
+ const response = await ctx.request('/api/demo/progressive-enhancement', {
27
27
  method: 'POST',
28
28
  headers: {
29
29
  'content-type': 'application/x-www-form-urlencoded'
@@ -35,7 +35,7 @@ test('native form submissions redirect back to the document route', async () =>
35
35
  assert.equal(response.status, 303);
36
36
  assert.equal(
37
37
  response.headers.get('location'),
38
- '/demo/progressive-enhancement?source=redirect&name=Native%20Flow'
38
+ '/api/demo/progressive-enhancement?source=redirect&name=Native%20Flow'
39
39
  );
40
40
  assert.equal(response.headers.get('x-webstir-fragment-target'), null);
41
41
  assert.equal(response.headers.get('content-type'), null);
@@ -43,7 +43,7 @@ test('native form submissions redirect back to the document route', async () =>
43
43
 
44
44
  test('redirected document route preserves the no-javascript form flow', async () => {
45
45
  const ctx = requireBackendTestContext();
46
- const response = await ctx.request('/demo/progressive-enhancement?source=redirect&name=Native%20Flow');
46
+ const response = await ctx.request('/api/demo/progressive-enhancement?source=redirect&name=Native%20Flow');
47
47
  const html = await response.text();
48
48
 
49
49
  assert.equal(response.status, 200);
@@ -55,7 +55,7 @@ test('redirected document route preserves the no-javascript form flow', async ()
55
55
 
56
56
  test('enhanced form submissions return fragment metadata and html', async () => {
57
57
  const ctx = requireBackendTestContext();
58
- const response = await ctx.request('/demo/progressive-enhancement', {
58
+ const response = await ctx.request('/api/demo/progressive-enhancement', {
59
59
  method: 'POST',
60
60
  headers: {
61
61
  'content-type': 'application/x-www-form-urlencoded',
@@ -80,7 +80,7 @@ test('enhanced form submissions return fragment metadata and html', async () =>
80
80
 
81
81
  test('native session sign-in redirects and sets a session cookie', async () => {
82
82
  const ctx = requireBackendTestContext();
83
- const response = await ctx.request('/demo/progressive-enhancement/session/sign-in', {
83
+ const response = await ctx.request('/api/demo/progressive-enhancement/session/sign-in', {
84
84
  method: 'POST',
85
85
  headers: {
86
86
  'content-type': 'application/x-www-form-urlencoded'
@@ -90,13 +90,13 @@ test('native session sign-in redirects and sets a session cookie', async () => {
90
90
  });
91
91
 
92
92
  assert.equal(response.status, 303);
93
- assert.equal(response.headers.get('location'), '/demo/progressive-enhancement?session=signed-in');
93
+ assert.equal(response.headers.get('location'), '/api/demo/progressive-enhancement?session=signed-in');
94
94
  assert.isTrue(String(response.headers.get('set-cookie')).includes('webstir_demo_session=Casey%20Proxy'));
95
95
  });
96
96
 
97
97
  test('enhanced session sign-in returns a fragment and persists on the next document request', async () => {
98
98
  const ctx = requireBackendTestContext();
99
- const response = await ctx.request('/demo/progressive-enhancement/session/sign-in', {
99
+ const response = await ctx.request('/api/demo/progressive-enhancement/session/sign-in', {
100
100
  method: 'POST',
101
101
  headers: {
102
102
  'content-type': 'application/x-www-form-urlencoded',
@@ -115,7 +115,7 @@ test('enhanced session sign-in returns a fragment and persists on the next docum
115
115
  assert.isTrue(html.includes('Signed in as <strong>Casey Proxy</strong>'));
116
116
  assert.isTrue(html.includes('id="demo-sign-out"'));
117
117
 
118
- const documentResponse = await ctx.request('/demo/progressive-enhancement', {
118
+ const documentResponse = await ctx.request('/api/demo/progressive-enhancement', {
119
119
  headers: {
120
120
  cookie
121
121
  }
@@ -129,7 +129,7 @@ test('enhanced session sign-in returns a fragment and persists on the next docum
129
129
 
130
130
  test('enhanced session sign-out returns a fragment and clears the session cookie', async () => {
131
131
  const ctx = requireBackendTestContext();
132
- const signInResponse = await ctx.request('/demo/progressive-enhancement/session/sign-in', {
132
+ const signInResponse = await ctx.request('/api/demo/progressive-enhancement/session/sign-in', {
133
133
  method: 'POST',
134
134
  headers: {
135
135
  'content-type': 'application/x-www-form-urlencoded',
@@ -139,7 +139,7 @@ test('enhanced session sign-out returns a fragment and clears the session cookie
139
139
  });
140
140
  const cookie = requireCookie(signInResponse.headers.get('set-cookie'));
141
141
 
142
- const response = await ctx.request('/demo/progressive-enhancement/session/sign-out', {
142
+ const response = await ctx.request('/api/demo/progressive-enhancement/session/sign-out', {
143
143
  method: 'POST',
144
144
  headers: {
145
145
  'x-webstir-client-nav': '1',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webstir-io/webstir",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webstir": "src/cli.ts"
@@ -46,8 +46,8 @@
46
46
  "@modelcontextprotocol/sdk": "1.28.0",
47
47
  "@webstir-io/module-contract": "^0.1.16",
48
48
  "@webstir-io/testing-contract": "^0.1.8",
49
- "@webstir-io/webstir-backend": "^0.1.16",
50
- "@webstir-io/webstir-frontend": "^0.1.41",
49
+ "@webstir-io/webstir-backend": "^0.1.17",
50
+ "@webstir-io/webstir-frontend": "^0.1.42",
51
51
  "@webstir-io/webstir-testing": "^0.1.6",
52
52
  "typescript": "^5.7.2",
53
53
  "zod": "^3.23.8"
@@ -17,6 +17,13 @@ const watchBrowserTestFiles = [
17
17
  const publishModeFilter = 'publish mode';
18
18
  const watchModeFilter = 'watch mode';
19
19
 
20
+ function buildSingleFileTestSteps(labelPrefix, files) {
21
+ return files.map((file) => ({
22
+ label: `${labelPrefix}: ${path.basename(file)}`,
23
+ args: ['test', '--bail=1', file],
24
+ }));
25
+ }
26
+
20
27
  export function listCoreTestFiles() {
21
28
  return readdirSync(testsDir)
22
29
  .filter((file) => file.endsWith('.ts'))
@@ -27,26 +34,23 @@ export function listCoreTestFiles() {
27
34
  }
28
35
 
29
36
  export function buildTestPlan(mode) {
30
- const coreTests = {
31
- label: 'core orchestrator tests',
32
- args: ['test', '--bail=1', ...listCoreTestFiles()],
33
- };
37
+ const coreTests = buildSingleFileTestSteps('core orchestrator test', listCoreTestFiles());
34
38
  const publishBrowserTests = {
35
39
  label: 'browser publish proofs',
36
40
  args: ['test', '--bail=1', browserTestFile, '-t', publishModeFilter],
37
41
  };
38
- const integrationWatchBrowserTests = {
39
- label: 'browser watch integration proofs',
40
- args: ['test', '--bail=1', ...watchBrowserTestFiles],
41
- };
42
+ const integrationWatchBrowserTests = buildSingleFileTestSteps(
43
+ 'browser watch integration proof',
44
+ watchBrowserTestFiles,
45
+ );
42
46
  const watchBrowserTests = {
43
47
  label: 'browser watch proofs',
44
48
  args: ['test', '--bail=1', browserTestFile, '-t', watchModeFilter],
45
49
  };
46
50
  const requiredPlan = [
47
- coreTests,
51
+ ...coreTests,
48
52
  publishBrowserTests,
49
- integrationWatchBrowserTests,
53
+ ...integrationWatchBrowserTests,
50
54
  watchBrowserTests,
51
55
  ];
52
56
 
@@ -56,7 +60,7 @@ export function buildTestPlan(mode) {
56
60
  case 'publish-browser':
57
61
  return [publishBrowserTests];
58
62
  case 'watch-browser':
59
- return [integrationWatchBrowserTests, watchBrowserTests];
63
+ return [...integrationWatchBrowserTests, watchBrowserTests];
60
64
  case 'all':
61
65
  case 'with-watch-browser':
62
66
  return requiredPlan;
@@ -1,13 +1,14 @@
1
- import type {
2
- AddJobOptions,
3
- AddRouteOptions,
4
- UpdateRouteContractOptions as BackendUpdateRouteContractOptions,
1
+ import {
2
+ runAddJob as runBackendAddJob,
3
+ runAddRoute as runBackendAddRoute,
4
+ runUpdateRouteContract as runBackendUpdateRouteContract,
5
+ type AddJobOptions,
6
+ type AddRouteOptions,
7
+ type UpdateRouteContractOptions as BackendUpdateRouteContractOptions,
5
8
  } from '@webstir-io/webstir-backend';
6
9
 
7
10
  import type { AddCommandResult } from './add.ts';
8
11
 
9
- import { monorepoRoot } from './paths.ts';
10
-
11
12
  export interface RunAddBackendOptions {
12
13
  readonly workspaceRoot: string;
13
14
  readonly rawArgs: readonly string[];
@@ -27,8 +28,7 @@ export async function runAddRouteCommand(options: RunAddBackendOptions): Promise
27
28
  export async function runAddRouteScaffold(
28
29
  options: AddRouteScaffoldOptions,
29
30
  ): Promise<AddCommandResult> {
30
- const backendAdd = await loadBackendAddModule();
31
- const result = await backendAdd.runAddRoute(options);
31
+ const result = await runBackendAddRoute(options);
32
32
 
33
33
  return {
34
34
  workspaceRoot: options.workspaceRoot,
@@ -46,8 +46,7 @@ export async function runAddJobCommand(options: RunAddBackendOptions): Promise<A
46
46
  }
47
47
 
48
48
  export async function runAddJobScaffold(options: AddJobScaffoldOptions): Promise<AddCommandResult> {
49
- const backendAdd = await loadBackendAddModule();
50
- const result = await backendAdd.runAddJob(options);
49
+ const result = await runBackendAddJob(options);
51
50
 
52
51
  return {
53
52
  workspaceRoot: options.workspaceRoot,
@@ -60,8 +59,7 @@ export async function runAddJobScaffold(options: AddJobScaffoldOptions): Promise
60
59
  export async function runUpdateRouteContract(
61
60
  options: UpdateRouteContractOptions,
62
61
  ): Promise<AddCommandResult> {
63
- const backendAdd = await loadBackendAddModule();
64
- const result = await backendAdd.runUpdateRouteContract(options);
62
+ const result = await runBackendUpdateRouteContract(options);
65
63
 
66
64
  return {
67
65
  workspaceRoot: options.workspaceRoot,
@@ -167,66 +165,6 @@ interface ParsedBackendCommandArgs {
167
165
  readonly booleans: ReadonlySet<string>;
168
166
  }
169
167
 
170
- interface BackendAddResult {
171
- readonly target: string;
172
- readonly changes: readonly string[];
173
- }
174
-
175
- interface BackendAddModule {
176
- readonly runAddRoute: (options: AddRouteOptions) => Promise<BackendAddResult>;
177
- readonly runAddJob: (options: AddJobOptions) => Promise<BackendAddResult>;
178
- readonly runUpdateRouteContract: (
179
- options: UpdateRouteContractOptions,
180
- ) => Promise<BackendAddResult>;
181
- }
182
-
183
- let backendAddModulePromise: Promise<BackendAddModule> | null = null;
184
-
185
- async function loadBackendAddModule(): Promise<BackendAddModule> {
186
- if (backendAddModulePromise) {
187
- return await backendAddModulePromise;
188
- }
189
-
190
- backendAddModulePromise = import('@webstir-io/webstir-backend').then(async (module) => {
191
- if (
192
- typeof module.runAddRoute === 'function' &&
193
- typeof module.runAddJob === 'function' &&
194
- typeof module.runUpdateRouteContract === 'function'
195
- ) {
196
- return module as BackendAddModule;
197
- }
198
-
199
- if (monorepoRoot) {
200
- throw new Error(
201
- 'Installed @webstir-io/webstir-backend package does not export runAddRoute/runAddJob/runUpdateRouteContract.',
202
- );
203
- }
204
-
205
- const compat = await import('./add-backend-compat.ts');
206
- return {
207
- async runAddRoute(options: AddRouteOptions): Promise<BackendAddResult> {
208
- return await compat.runAddRoute(options);
209
- },
210
- async runAddJob(options: AddJobOptions): Promise<BackendAddResult> {
211
- return await compat.runAddJob({
212
- workspaceRoot: options.workspaceRoot,
213
- name: options.name,
214
- schedule: options.schedule,
215
- description: options.description,
216
- ...(options.priority !== undefined ? { priority: String(options.priority) } : {}),
217
- });
218
- },
219
- async runUpdateRouteContract(): Promise<BackendAddResult> {
220
- throw new Error(
221
- 'Installed @webstir-io/webstir-backend package does not export runUpdateRouteContract.',
222
- );
223
- },
224
- };
225
- });
226
-
227
- return await backendAddModulePromise;
228
- }
229
-
230
168
  function parseBackendCommandArgs(
231
169
  rawArgs: readonly string[],
232
170
  spec: ParseSpec,
@@ -57,15 +57,10 @@ export function createBunFrontendFetchHandler(options: BunFrontendFetchHandlerOp
57
57
  }
58
58
 
59
59
  function getApiProxyPath(pathname: string): string | null {
60
- if (pathname === '/api') {
61
- return '/';
60
+ const normalizedPath = pathname.startsWith('/') ? path.posix.normalize(pathname) : null;
61
+ if (normalizedPath === '/api' || normalizedPath?.startsWith('/api/')) {
62
+ return normalizedPath;
62
63
  }
63
-
64
- if (pathname.startsWith('/api/')) {
65
- const normalizedPath = path.posix.normalize(pathname.slice('/api'.length));
66
- return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
67
- }
68
-
69
64
  return null;
70
65
  }
71
66
 
@@ -132,7 +127,7 @@ function rewriteProxyLocation(value: string, targetUrl: URL): string {
132
127
  }
133
128
 
134
129
  if (trimmed.startsWith('/')) {
135
- return prefixApiMount(trimmed);
130
+ return trimmed;
136
131
  }
137
132
 
138
133
  try {
@@ -140,20 +135,12 @@ function rewriteProxyLocation(value: string, targetUrl: URL): string {
140
135
  if (resolved.origin !== targetUrl.origin) {
141
136
  return value;
142
137
  }
143
- return prefixApiMount(`${resolved.pathname}${resolved.search}${resolved.hash}`);
138
+ return `${resolved.pathname}${resolved.search}${resolved.hash}`;
144
139
  } catch {
145
140
  return value;
146
141
  }
147
142
  }
148
143
 
149
- function prefixApiMount(pathname: string): string {
150
- if (pathname === '/api' || pathname.startsWith('/api/')) {
151
- return pathname;
152
- }
153
-
154
- return pathname === '/' ? '/api' : `/api${pathname}`;
155
- }
156
-
157
144
  function methodAllowsBody(method: string): boolean {
158
145
  return method !== 'GET' && method !== 'HEAD';
159
146
  }
package/src/dev-server.ts CHANGED
@@ -303,15 +303,10 @@ export function getStaticCandidatePaths(pathname: string): readonly string[] {
303
303
  }
304
304
 
305
305
  export function getApiProxyPath(pathname: string): string | null {
306
- if (pathname === '/api') {
307
- return '/';
306
+ const normalizedPath = pathname.startsWith('/') ? path.posix.normalize(pathname) : null;
307
+ if (normalizedPath === '/api' || normalizedPath?.startsWith('/api/')) {
308
+ return normalizedPath;
308
309
  }
309
-
310
- if (pathname.startsWith('/api/')) {
311
- const normalizedPath = path.posix.normalize(pathname.slice('/api'.length));
312
- return normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`;
313
- }
314
-
315
310
  return null;
316
311
  }
317
312
 
@@ -357,7 +352,7 @@ function rewriteProxyLocation(value: string, targetUrl: URL): string {
357
352
  }
358
353
 
359
354
  if (trimmed.startsWith('/')) {
360
- return prefixApiMount(trimmed);
355
+ return trimmed;
361
356
  }
362
357
 
363
358
  try {
@@ -365,20 +360,12 @@ function rewriteProxyLocation(value: string, targetUrl: URL): string {
365
360
  if (resolved.origin !== targetUrl.origin) {
366
361
  return value;
367
362
  }
368
- return prefixApiMount(`${resolved.pathname}${resolved.search}${resolved.hash}`);
363
+ return `${resolved.pathname}${resolved.search}${resolved.hash}`;
369
364
  } catch {
370
365
  return value;
371
366
  }
372
367
  }
373
368
 
374
- function prefixApiMount(pathname: string): string {
375
- if (pathname === '/api' || pathname.startsWith('/api/')) {
376
- return pathname;
377
- }
378
-
379
- return pathname === '/' ? '/api' : `/api${pathname}`;
380
- }
381
-
382
369
  async function resolveStaticFile(
383
370
  buildRoot: string,
384
371
  relativePaths: readonly string[],
@@ -434,7 +421,7 @@ function setCacheHeaders(headers: Headers, relativePath: string): void {
434
421
  }
435
422
 
436
423
  const extension = path.extname(relativePath).toLowerCase();
437
- if (extension === '.html' || extension === '') {
424
+ if (extension === '.html' || extension === '' || extension === '.json') {
438
425
  setNoCacheHeaders(headers);
439
426
  return;
440
427
  }
package/src/execute.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  CommandMode,
11
11
  } from './types.ts';
12
12
  import { readWorkspaceDescriptor } from './workspace.ts';
13
+ import { assertNoActiveWorkspaceWatch } from './workspace-lock.ts';
13
14
 
14
15
  export interface RunCommandOptions {
15
16
  readonly workspaceRoot: string;
@@ -22,6 +23,7 @@ export async function runCommand(
22
23
  options: RunCommandOptions,
23
24
  ): Promise<CommandExecutionResult> {
24
25
  const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
26
+ await assertNoActiveWorkspaceWatch(workspace.root, mode);
25
27
  const providerLoader = options.loadProvider ?? loadProvider;
26
28
  const targets = [];
27
29
 
package/src/watch.ts CHANGED
@@ -4,6 +4,7 @@ import { runFrontendWatch } from './frontend-watch.ts';
4
4
  import { runFullWatch } from './full-watch.ts';
5
5
  import type { WorkspaceDescriptor } from './types.ts';
6
6
  import { readWorkspaceDescriptor } from './workspace.ts';
7
+ import { acquireWorkspaceWatchLock } from './workspace-lock.ts';
7
8
 
8
9
  interface WatchStream {
9
10
  write(message: string): void;
@@ -42,24 +43,30 @@ const defaultIo: WatchIo = {
42
43
 
43
44
  export async function runWatch(options: RunWatchOptions): Promise<void> {
44
45
  const io = options.io ?? defaultIo;
45
- await materializeRepoLocalWorkspaceDependencies(options.workspaceRoot);
46
46
  const workspace = await readWorkspaceDescriptor(options.workspaceRoot);
47
+ const watchLock = await acquireWorkspaceWatchLock(workspace.root);
47
48
 
48
- switch (workspace.mode) {
49
- case 'spa':
50
- await runFrontendWatch(workspace, options, io);
51
- return;
52
- case 'ssg':
53
- await runFrontendWatch(workspace, options, io);
54
- return;
55
- case 'api':
56
- await runApiWatch(workspace, options, io);
57
- return;
58
- case 'full':
59
- await runFullWatch(workspace, options, io);
60
- return;
61
- default:
62
- throwUnsupportedWatchMode(workspace);
49
+ try {
50
+ await materializeRepoLocalWorkspaceDependencies(options.workspaceRoot);
51
+
52
+ switch (workspace.mode) {
53
+ case 'spa':
54
+ await runFrontendWatch(workspace, options, io);
55
+ return;
56
+ case 'ssg':
57
+ await runFrontendWatch(workspace, options, io);
58
+ return;
59
+ case 'api':
60
+ await runApiWatch(workspace, options, io);
61
+ return;
62
+ case 'full':
63
+ await runFullWatch(workspace, options, io);
64
+ return;
65
+ default:
66
+ throwUnsupportedWatchMode(workspace);
67
+ }
68
+ } finally {
69
+ await watchLock.release();
63
70
  }
64
71
  }
65
72