From 0e7ccf4b962f91efbbd781a3c67ab8fcbcb80bea Mon Sep 17 00:00:00 2001 From: Repraance Date: Thu, 9 Jun 2022 08:18:13 +0800 Subject: [PATCH 1/2] feat(createFile): support async content --- .../file-presets/files/app/user/app.js.ts | 19 ++-- .../file-presets/files/app/user/error.js.ts | 19 ++-- .../file-presets/files/app/user/runtime.js.ts | 19 ++-- .../src/lib/features/html-render/index.ts | 26 +++-- packages/service/src/core/api.ts | 1 - packages/service/src/core/lifecycle.ts | 2 - .../file-manager/__tests__/createFile.test.ts | 100 +++++++++++++++++- .../file-manager/createFile/createFile.ts | 97 ++++++++++++----- .../src/project/file-manager/fileTypes.ts | 2 +- .../service/src/project/file-manager/mount.ts | 11 +- 10 files changed, 225 insertions(+), 71 deletions(-) diff --git a/packages/platform-shared/src/project/file-presets/files/app/user/app.js.ts b/packages/platform-shared/src/project/file-presets/files/app/user/app.js.ts index 3a54f8fe4..3b217b77f 100644 --- a/packages/platform-shared/src/project/file-presets/files/app/user/app.js.ts +++ b/packages/platform-shared/src/project/file-presets/files/app/user/app.js.ts @@ -3,14 +3,13 @@ import { ProjectContext } from '../../../../projectContext'; const { getAllFiles, getFirstModuleExport } = fileUtils; -export default (context: ProjectContext) => - createFileWithoutName(() => { - const { app } = context.userModule; - const candidates = Array.isArray(app) ? app : [app]; - return { - dependencies: candidates, - content: () => { - return getFirstModuleExport(getAllFiles(candidates), candidates, true); - } - }; +export default (context: ProjectContext) => { + const { app } = context.userModule; + const candidates = Array.isArray(app) ? app : [app]; + return createFileWithoutName({ + dependencies: candidates, + content: () => { + return getFirstModuleExport(getAllFiles(candidates), candidates, true); + } }); +}; diff --git a/packages/platform-shared/src/project/file-presets/files/app/user/error.js.ts b/packages/platform-shared/src/project/file-presets/files/app/user/error.js.ts index 38b7108bb..50a36ad51 100644 --- a/packages/platform-shared/src/project/file-presets/files/app/user/error.js.ts +++ b/packages/platform-shared/src/project/file-presets/files/app/user/error.js.ts @@ -3,14 +3,13 @@ import { ProjectContext } from '../../../../projectContext'; const { getAllFiles, getFirstModuleExport } = fileUtils; -export default (context: ProjectContext) => - createFileWithoutName(() => { - const { error } = context.userModule; - const candidates = Array.isArray(error) ? error : [error]; - return { - dependencies: candidates, - content: () => { - return getFirstModuleExport(getAllFiles(candidates), candidates, true); - } - }; +export default (context: ProjectContext) => { + const { error } = context.userModule; + const candidates = Array.isArray(error) ? error : [error]; + return createFileWithoutName({ + dependencies: candidates, + content: () => { + return getFirstModuleExport(getAllFiles(candidates), candidates, true); + } }); +}; diff --git a/packages/platform-shared/src/project/file-presets/files/app/user/runtime.js.ts b/packages/platform-shared/src/project/file-presets/files/app/user/runtime.js.ts index f59641bd9..cd217ef07 100644 --- a/packages/platform-shared/src/project/file-presets/files/app/user/runtime.js.ts +++ b/packages/platform-shared/src/project/file-presets/files/app/user/runtime.js.ts @@ -3,14 +3,13 @@ import { ProjectContext } from '../../../../projectContext'; const { getAllFiles, getFirstModuleExport } = fileUtils; -export default (context: ProjectContext) => - createFileWithoutName(() => { - const { runtime } = context.userModule; - const candidates = Array.isArray(runtime) ? runtime : [runtime]; - return { - dependencies: candidates, - content: () => { - return getFirstModuleExport(getAllFiles(candidates), candidates); - } - }; +export default (context: ProjectContext) => { + const { runtime } = context.userModule; + const candidates = Array.isArray(runtime) ? runtime : [runtime]; + return createFileWithoutName({ + dependencies: candidates, + content: () => { + return getFirstModuleExport(getAllFiles(candidates), candidates); + } }); +}; diff --git a/packages/platform-web/src/lib/features/html-render/index.ts b/packages/platform-web/src/lib/features/html-render/index.ts index de7da6d27..fa0e8c968 100644 --- a/packages/platform-web/src/lib/features/html-render/index.ts +++ b/packages/platform-web/src/lib/features/html-render/index.ts @@ -8,7 +8,8 @@ import { getFirstModuleExport, getAllFiles } from '@shuvi/service/lib/project/file-utils'; -import { build } from '@shuvi/toolpack/lib/utils/build-loaders'; +import { buildToString } from '@shuvi/toolpack/lib/utils/build-loaders'; +import * as fs from 'fs'; import * as path from 'path'; import { extendedHooks } from './hooks'; import { @@ -146,17 +147,30 @@ const core = createPlugin({ }, dependencies: serverCandidates }); + const loadersFileName = path.join( + context.paths.appDir, + 'files', + 'loaders.js' + ); + const loadersBuildFile = createFile({ + name: 'loaders-build.js', + content: async () => { + if (fs.existsSync(loadersFileName)) { + return await buildToString(loadersFileName); + } + return ''; + }, + dependencies: [paths.pagesDir, loadersFileName] + }); return [ + userDocumentFile, routerConfigFile, routesFile, userServerFile, - userDocumentFile, - loadersFile + loadersFile, + loadersBuildFile ]; }, - afterShuviAppBuild: async context => { - await build(path.join(context.paths.appDir, 'files'), context.mode); - }, addRuntimeService: () => [ { source: require.resolve( diff --git a/packages/service/src/core/api.ts b/packages/service/src/core/api.ts index b1ef52009..dafe58e08 100644 --- a/packages/service/src/core/api.ts +++ b/packages/service/src/core/api.ts @@ -185,7 +185,6 @@ class Api { this._initArtifacts() ]); await this._projectBuilder.build(this._paths.privateDir); - await this.pluginManager.runner.afterShuviAppBuild(); } addRuntimeFile(options: FileOptions): void { diff --git a/packages/service/src/core/lifecycle.ts b/packages/service/src/core/lifecycle.ts index 32e620d72..d9e4c8a15 100644 --- a/packages/service/src/core/lifecycle.ts +++ b/packages/service/src/core/lifecycle.ts @@ -23,7 +23,6 @@ import { import { IPluginContext } from './apiTypes'; const afterInit = createAsyncParallelHook(); -const afterShuviAppBuild = createAsyncParallelHook(); const afterBuild = createAsyncParallelHook(); const afterDestroy = createAsyncParallelHook(); const afterBundlerDone = createAsyncParallelHook(); @@ -58,7 +57,6 @@ const addRuntimeService = createAsyncParallelHook< const builtinPluginHooks = { afterInit, - afterShuviAppBuild, afterBuild, afterDestroy, afterBundlerDone, diff --git a/packages/service/src/project/file-manager/__tests__/createFile.test.ts b/packages/service/src/project/file-manager/__tests__/createFile.test.ts index d534e1a9d..371e5f424 100644 --- a/packages/service/src/project/file-manager/__tests__/createFile.test.ts +++ b/packages/service/src/project/file-manager/__tests__/createFile.test.ts @@ -49,7 +49,7 @@ afterEach(async () => { describe('createFile', () => { describe('should work with watching files', () => { - describe('should work without using context', () => { + describe('should work without using context, and sync content', () => { test('should update when single dependency file update', async () => { const fileManager = getFileManager({ watch: true }); fileManager.addFile( @@ -145,6 +145,104 @@ describe('createFile', () => { }); }); + describe('should work without using context, and async content', () => { + test('should update when single dependency file update', async () => { + const fileManager = getFileManager({ watch: true }); + fileManager.addFile( + createFile({ + name: FILE_RESULT, + content() { + return fs.readFileSync(fileA, 'utf8') as string; + }, + dependencies: fileA + }) + ); + await fileManager.mount(resolveFixture('createFile')); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe('a\n'); + fs.writeFileSync(fileA, 'aa\n', 'utf8'); + await wait(500); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe('aa\n'); + await fileManager.unmount(); + }); + + test('should update when multiple dependency files update', async () => { + const fileManager = getFileManager({ watch: true }); + const dependencies = [fileA, fileB, unexistedFileC]; + fileManager.addFile( + createFile({ + name: FILE_RESULT, + async content() { + await wait(1000); + let content = ''; + dependencies.forEach(file => { + if (fs.existsSync(file)) { + content += fs.readFileSync(file, 'utf8'); + } + }); + return content; + }, + dependencies + }) + ); + await fileManager.mount(resolveFixture('createFile')); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe('a\nb\n'); + fs.writeFileSync(fileA, 'aa\n', 'utf8'); + fs.writeFileSync(fileB, 'bb\n', 'utf8'); + await wait(1200); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe('aa\nbb\n'); + fs.writeFileSync(unexistedFileC, 'cc\n', 'utf8'); + await wait(1200); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe('aa\nbb\ncc\n'); + await fileManager.unmount(); + await safeDelete(unexistedFileC); + }); + + test('should update when dependency files and directories update', async () => { + const fileManager = getFileManager({ watch: true }); + const dependencies = [ + fileA, + fileB, + unexistedFileC, + directoryA, + directoryB + ]; + fileManager.addFile( + createFile({ + name: FILE_RESULT, + async content() { + await wait(1000); + let content = ''; + const allFiles = getAllFiles(dependencies); + allFiles.forEach(file => { + if (fs.existsSync(file)) { + content += fs.readFileSync(file, 'utf8'); + } + }); + return content; + }, + dependencies + }) + ); + await fileManager.mount(resolveFixture('createFile')); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe( + 'a\nb\ndaa\ndab\n' + ); + fs.writeFileSync(fileA, 'aa\n', 'utf8'); + fs.writeFileSync(fileB, 'bb\n', 'utf8'); + fs.writeFileSync(unexistedFileC, 'cc\n', 'utf8'); + fs.writeFileSync(daa, 'daaa\n', 'utf8'); + fs.writeFileSync(dba, 'dba\n', 'utf8'); + await wait(1200); + expect(fs.readFileSync(file(FILE_RESULT), 'utf8')).toBe( + 'aa\nbb\ncc\ndaaa\ndab\ndba\n' + ); + await fileManager.unmount(); + await safeDelete(unexistedFileC); + await safeDelete(dba); + fs.writeFileSync(daa, 'daa\n', 'utf8'); + }); + }); + describe('should work with using context', () => { test('should update when context as dependencies', async () => { const context = reactive({ diff --git a/packages/service/src/project/file-manager/createFile/createFile.ts b/packages/service/src/project/file-manager/createFile/createFile.ts index 3ec8d70a2..981093b3d 100644 --- a/packages/service/src/project/file-manager/createFile/createFile.ts +++ b/packages/service/src/project/file-manager/createFile/createFile.ts @@ -1,60 +1,79 @@ import * as fs from 'fs'; import { reactive } from '@vue/reactivity'; import { watch } from '@shuvi/utils/lib/fileWatcher'; - import { FileOptions } from '../file'; export type FileOptionsWithoutName = Omit; export type CreateFileOption = { name: string; - content: () => string; + content: () => string | Promise; dependencies?: string | string[]; }; -export type CreateFileOptionWithContext = { +export type CreateFileOptionSyncWithContext = { name: string; content: (context: C) => string; + dependencies?: string | string[]; }; -export type CreateFileOptionWithoutName = Omit; -export type CreateFileOptionWithContextWithoutName = Omit< - CreateFileOptionWithContext, - 'name' ->; +export type CreateFileOptionSync = { + content: () => string; + dependencies?: string | string[]; +}; /** used by file-presets because their names are dynamically generated by their paths*/ -export function createFileWithoutName( - options: CreateFileOptionWithContextWithoutName + +/** options without context async */ +export function createFileWithoutName( + options: Omit ): FileOptionsWithoutName; -export function createFileWithoutName( - initializer: (context: C) => CreateFileOptionWithoutName + +/** options with context sync*/ +export function createFileWithoutName( + options: Omit, 'name'> +): FileOptionsWithoutName; + +/** initializer sync*/ +export function createFileWithoutName( + initializer: (context: C) => CreateFileOptionSync ): FileOptionsWithoutName; -export function createFileWithoutName(options: any): any { + +export function createFileWithoutName(options: any): any { return createFile(options); } +/** options without context async */ export function createFile(options: CreateFileOption): FileOptions; + +/** options with context sync*/ export function createFile( - options: CreateFileOptionWithContext + options: CreateFileOptionSyncWithContext ): FileOptions; + +/** initializer sync*/ export function createFile( - initializer: (context: C) => CreateFileOptionWithoutName, + initializer: (context: C) => CreateFileOptionSync, name: string ): FileOptions; + export function createFile(options: any, name?: string): any { let fileState: { content: string }; + let fileContent: string; + let initiated = false; let watcher: () => void; let getWatcher: () => () => void; - const mounted = () => { + let instanceThis: any; + const mounted = function (this: any) { watcher = getWatcher(); + instanceThis = this._; }; const unmounted = () => { watcher(); }; if (typeof options === 'function') { - const getContent = (context: C) => { - const createFileOptions = options(context) as CreateFileOptionWithoutName; + const getContent = function (context: C) { + const createFileOptions = options(context) as CreateFileOptionSync; const { dependencies = [], content } = createFileOptions; const files: string[] = []; const directories: string[] = []; @@ -73,10 +92,11 @@ export function createFile(options: any, name?: string): any { missing.push(filepath); } }); - if (!fileState) { + if (!initiated) { fileState = reactive({ content: content() }); + initiated = true; } else { fileState.content = content(); } @@ -98,7 +118,9 @@ export function createFile(options: any, name?: string): any { unmounted }; } - const { dependencies = [], content } = options as CreateFileOption; + const { dependencies = [], content } = options as + | CreateFileOption + | CreateFileOptionSyncWithContext; const files: string[] = []; const directories: string[] = []; const missing: string[] = []; @@ -119,17 +141,40 @@ export function createFile(options: any, name?: string): any { missing.push(filepath); } }); - fileState = reactive({ - content: content() - }); + let isPromise = false; getWatcher = () => { - return watch({ files, directories, missing }, () => { - fileState.content = content(); + return watch({ files, directories, missing }, async () => { + if (isPromise) { + fileContent = await content(instanceThis?.ctx); + instanceThis?.update(); + } else { + fileState.content = content(instanceThis?.ctx) as string; + } }); }; + + const getContent = (context: C) => { + if (!initiated) { + const contentResult = content(context); + if (contentResult instanceof Promise) { + isPromise = true; + return contentResult.then(result => { + initiated = true; + fileContent = result; + return fileContent; + }); + } else { + fileState = reactive({ + content: contentResult + }); + initiated = true; + } + } + return isPromise ? fileContent : fileState.content; + }; return { name: options.name, - content: () => fileState.content, + content: getContent, mounted, unmounted }; diff --git a/packages/service/src/project/file-manager/fileTypes.ts b/packages/service/src/project/file-manager/fileTypes.ts index 93e146531..750e130d1 100644 --- a/packages/service/src/project/file-manager/fileTypes.ts +++ b/packages/service/src/project/file-manager/fileTypes.ts @@ -18,7 +18,7 @@ export interface FileOptionsBase content: ( this: CreateFilePublicInstance, ctx: any - ) => string | null | undefined; + ) => Promise | string | null | undefined; // state // Limitation: we cannot expose RawBindings on the `this` context for data diff --git a/packages/service/src/project/file-manager/mount.ts b/packages/service/src/project/file-manager/mount.ts index 9d6cf25db..4c5f64ca5 100644 --- a/packages/service/src/project/file-manager/mount.ts +++ b/packages/service/src/project/file-manager/mount.ts @@ -18,12 +18,12 @@ export function mount( const { content, name: fsPath, setupState } = instance; const dir = path.dirname(fsPath); let fd: any; - const componentEffect = () => { + const componentEffect = async () => { // mount if (!instance.isMounted) { fse.ensureDirSync(dir); fd = fse.openSync(fsPath, 'w+'); - const fileContent = content(context, setupState); + const fileContent = await content(context, setupState); if (fileContent != undefined) { fse.writeSync(fd, fileContent, 0); } @@ -37,14 +37,17 @@ export function mount( defer.resolve(instance); return; } - const fileContent = content(context, setupState); + const fileContent = await content(context, setupState); fse.ftruncateSync(fd, 0); fse.writeSync(fd, fileContent, 0); }; if (watch) { instance.update = effect(componentEffect, { scheduler: queueJob, - allowRecurse: true + allowRecurse: true, + onStop: () => { + console.log('stopped'); + } // lazy: true, }); } else { From c579f89b432c86a4c621bd6b8db410a2eb1c2617 Mon Sep 17 00:00:00 2001 From: Repraance Date: Thu, 9 Jun 2022 09:04:17 +0800 Subject: [PATCH 2/2] fix: minor fixes --- .../project/file-manager/createFile/createFile.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/service/src/project/file-manager/createFile/createFile.ts b/packages/service/src/project/file-manager/createFile/createFile.ts index 981093b3d..2c21f1cdc 100644 --- a/packages/service/src/project/file-manager/createFile/createFile.ts +++ b/packages/service/src/project/file-manager/createFile/createFile.ts @@ -63,12 +63,13 @@ export function createFile(options: any, name?: string): any { let initiated = false; let watcher: () => void; let getWatcher: () => () => void; - let instanceThis: any; + let currentInstance: any; const mounted = function (this: any) { watcher = getWatcher(); - instanceThis = this._; + currentInstance = this._; }; const unmounted = () => { + currentInstance = null; watcher(); }; if (typeof options === 'function') { @@ -144,11 +145,12 @@ export function createFile(options: any, name?: string): any { let isPromise = false; getWatcher = () => { return watch({ files, directories, missing }, async () => { + if (!currentInstance) return if (isPromise) { - fileContent = await content(instanceThis?.ctx); - instanceThis?.update(); + fileContent = await content(currentInstance?.ctx); + currentInstance?.update(); } else { - fileState.content = content(instanceThis?.ctx) as string; + fileState.content = content(currentInstance?.ctx) as string; } }); };