Repository navigation
fix(portal): keep the preview up and report a broken specification while serving #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
55cf55b
78a69b8
2093544
c40192f
8df4a90
865ac82
ecd4b0e
02badfc
9291028
21a9e15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,9 +9,10 @@ import { ProjectContext } from '../../types/project-context.js'; | |
| import { isSkippedByGlob } from '../../types/portal/content-tree.js'; | ||
| import { GeneratedPages } from '../../types/portal/generated-pages.js'; | ||
| import { PortalArtifacts } from '../../types/portal/portal-artifacts.js'; | ||
| import { PortalSettings, PortalSource } from '../../types/portal/portal-source.js'; | ||
| import { PortalSettings, PortalSource, WatchedSource } from '../../types/portal/portal-source.js'; | ||
| import { PreviewConfig } from '../../types/portal/preview-config.js'; | ||
| import { PreviewContent } from '../../types/portal/preview-content.js'; | ||
| import { PreviewSpecs } from '../../types/portal/preview-specs.js'; | ||
| import { FileWatch, FileWatchService } from '../../infrastructure/file-watch-service.js'; | ||
| import { NetworkService } from '../../infrastructure/network-service.js'; | ||
| import { LauncherService } from '../../infrastructure/launcher-service.js'; | ||
|
|
@@ -78,7 +79,9 @@ export class PortalServeAction { | |
| contentWatch?.recheck(); | ||
| } | ||
| }); | ||
| const specWatch = this.watchSpecs(project, source); | ||
| const closeWatches = async () => { | ||
| await specWatch?.close(); | ||
| await configWatch?.close(); | ||
| await contentWatch?.close(); | ||
| }; | ||
|
|
@@ -147,25 +150,15 @@ export class PortalServeAction { | |
|
|
||
| const applied = await this.projectService.applyConfig(portalProjectDirectory, settings); | ||
| if (applied.isErr()) { | ||
| this.prompts.configNotApplied(applied.error); | ||
| this.prompts.notApplied('config', applied.error, sourceDirectory); | ||
| return; | ||
| } | ||
| if (preview.show(config, applied.value)) { | ||
| this.prompts.configApplied(); | ||
| } | ||
| this.prompts.configShown(preview.show(config, applied.value)); | ||
| onApplied(settings); | ||
| }; | ||
|
|
||
| return this.startWatch( | ||
| (onChange) => | ||
| this.fileWatchService.watch(sourceDirectory, new FileName(APIMATIC_CONFIG_FILE_NAME), onChange, (reason) => | ||
| this.prompts.configWatchFailed(reason) | ||
| ), | ||
| applyEdit, | ||
| { | ||
| notWatched: (reason) => this.prompts.configNotWatched(reason), | ||
| thrown: (reason) => this.prompts.configNotApplied(reason) | ||
| } | ||
| return this.startWatch('config', sourceDirectory, applyEdit, (onChange, onFailed) => | ||
| this.fileWatchService.watch(sourceDirectory, new FileName(APIMATIC_CONFIG_FILE_NAME), onChange, onFailed) | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -197,30 +190,37 @@ export class PortalServeAction { | |
| checked.value.files | ||
| ); | ||
| if (applied.isErr()) { | ||
| this.prompts.contentNotApplied(applied.error, sourceDirectory); | ||
| this.prompts.notApplied('content', applied.error, sourceDirectory); | ||
| return; | ||
| } | ||
| const shown = preview.show(checked.value.notices); | ||
| if (shown.fixed) { | ||
| this.prompts.contentAccepted(sourceDirectory); | ||
| } | ||
| this.prompts.contentNotices(shown.notices, sourceDirectory); | ||
| this.prompts.contentShown(preview.show(checked.value.notices), sourceDirectory); | ||
| }; | ||
|
|
||
| return this.startWatch( | ||
| (onChange) => | ||
| this.fileWatchService.watchTree( | ||
| contentDirectory, | ||
| onChange, | ||
| (reason) => this.prompts.contentWatchFailed(reason, sourceDirectory), | ||
| // What the build never reads, as an editor's swap file, would only have the check run again. | ||
| isSkippedByGlob | ||
| ), | ||
| check, | ||
| { | ||
| notWatched: (reason) => this.prompts.contentNotWatched(reason, sourceDirectory), | ||
| thrown: (reason) => this.prompts.contentNotChecked(reason, sourceDirectory) | ||
| // What the build never reads, as an editor's swap file, would only have the check run again. | ||
| return this.startWatch('content', sourceDirectory, check, (onChange, onFailed) => | ||
| this.fileWatchService.watchTree(contentDirectory, onChange, onFailed, isSkippedByGlob) | ||
| ); | ||
| } | ||
|
|
||
| /** Held to a build's rules, as `content/` is, but the preview itself leaves out a reference it cannot read. */ | ||
| private watchSpecs(project: ProjectContext, source: PortalSource): FileWatch | undefined { | ||
| const sourceDirectory = project.sourceDirectory(); | ||
| const sourceContext = project.portalSource(); | ||
| const preview = new PreviewSpecs(source.specs); | ||
|
|
||
| const check = async () => { | ||
| const checked = await sourceContext.resolveSpecs(); | ||
| if (checked.isErr()) { | ||
| preview.refuse(); | ||
| this.prompts.specRejected(checked.error, sourceDirectory); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two noisy cases, both reproducible on Windows:
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both come from timing, so I've left them and listed them in the description. The repeated refusal is a watch event landing while the check runs, which queues one more check; dropping a refusal identical to the last would also hide the answer to a real second save. For the slow delete-and-rewrite, at that moment the folder really has no definition, and the next save says it's fixed; telling a mid-save from a deletion would mean the watch waiting longer on every save. |
||
| return; | ||
| } | ||
| this.prompts.specsShown(preview.show(checked.value), sourceDirectory); | ||
| }; | ||
|
|
||
| // As for content: an editor's swap file, saved while a document is refused, would only repeat the refusal. | ||
| return this.startWatch('spec', sourceDirectory, check, (onChange, onFailed) => | ||
| this.fileWatchService.watchTree(source.specDirectory, onChange, onFailed, isSkippedByGlob) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On Windows, deleting I reproduced it with this watch, and with
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for tracing it to |
||
| ); | ||
| } | ||
|
|
||
|
|
@@ -229,21 +229,22 @@ export class PortalServeAction { | |
| * before the preview started, which can take a minute, and a save made meanwhile reached no watch. | ||
| */ | ||
| private startWatch( | ||
| watch: (onChange: () => Promise<void>) => Result<FileWatch, string>, | ||
| watched: WatchedSource, | ||
| sourceDirectory: DirectoryPath, | ||
| handle: () => Promise<void>, | ||
| report: { notWatched: (reason: string) => void; thrown: (reason: string) => void } | ||
| watch: (onChange: () => Promise<void>, onFailed: (reason: string) => void) => Result<FileWatch, string> | ||
| ): FileWatch | undefined { | ||
| // The watch drops whatever its handler throws, which would leave the preview stale without a word. | ||
| const onChange = async () => { | ||
| try { | ||
| await handle(); | ||
| } catch (error) { | ||
| report.thrown(errorMessage(error)); | ||
| this.prompts.notChecked(watched, errorMessage(error), sourceDirectory); | ||
| } | ||
| }; | ||
| const started = watch(onChange); | ||
| const started = watch(onChange, (reason) => this.prompts.watchFailed(watched, reason, sourceDirectory)); | ||
| if (started.isErr()) { | ||
| report.notWatched(started.error); | ||
| this.prompts.notWatched(watched, started.error, sourceDirectory); | ||
| return undefined; | ||
| } | ||
| started.value.recheck(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two follow-ups on this path:
console.errorgoes into the dev server's output, whichPortalDevServerServicecaptures and shows only after a crash. Whenever the CLI check passes butbundleSpecthrows (see the comment onlocalFile), nothing tells the user why the reference disappeared. It's worth getting this message to the terminal.prerenderPagesinvite.config.tsstill loads every spec withPromise.allwhen the config loads, undervite devtoo. A spec in that state therefore stops the preview from starting ("The portal preview stopped unexpectedly"). I confirmeddevdoes the same, so it isn't a regression, but "the preview survives a broken specification" only holds for edits made after startup.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$idcase, an unreachable URL, and what only the portal refuses, such as a duplicateoperationId. Getting the preview's own error into the terminal means picking lines out of the server's output, so I've left it as a follow-up; it's in the description.vite devno longer builds the prerender page list, so a spec the bundler fails on no longer stops the preview from starting. A new e2e test starts the preview with one (a$refbelow an$id); it failed with "stopped unexpectedly" before the fix.