Skip to content

Commit

Permalink
Merge branch 'master' of github.com:wixplosives/codux-core into nadav…
Browse files Browse the repository at this point in the history
…/handle-hash-navigation
  • Loading branch information
nadavov committed Oct 20, 2024
2 parents 65eb07b + bd8ae89 commit 5c1023d
Show file tree
Hide file tree
Showing 8 changed files with 463 additions and 282 deletions.
50 changes: 27 additions & 23 deletions packages/app-core/src/define-app-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ export interface FSApi {
path: PathApi;
}

export interface IReactApp<T = unknown> {
export interface IReactApp<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = unknown> {
/**
* Should be isomorphic, should return the same result on the server and in a web worker
* returns the app manifest containing a list of routes for the app.
*
* should call onManifestUpdate when the manifest is updated
*/
prepareApp: (options: IPrepareAppOptions) => Promise<{
manifest: IAppManifest<T>;
manifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>;
dispose: () => void;
}>;

Expand All @@ -106,13 +106,13 @@ export interface IReactApp<T = unknown> {
* returns the information needed to create a new page
*
*/
getNewPageInfo?: (options: IGetNewPageInfoOptions<T>) => {
getNewPageInfo?: (options: IGetNewPageInfoOptions<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>) => {
isValid: boolean;
errorMessage?: string;
warningMessage?: string;
pageModule: string;
newPageSourceCode: string;
newPageRoute?: RouteInfo<T>;
newPageRoute?: RouteInfo<ROUTE_EXTRA_DATA>;
routingPattern?: RoutingPattern;
};

Expand All @@ -121,12 +121,12 @@ export interface IReactApp<T = unknown> {
*
* returns the information needed to move a page
*/
getMovePageInfo?: (options: IMovePageInfoOptions<T>) => {
getMovePageInfo?: (options: IMovePageInfoOptions<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>) => {
isValid: boolean;
errorMessage?: string;
warningMessage?: string;
pageModule: string;
newPageRoute?: RouteInfo<T>;
newPageRoute?: RouteInfo<ROUTE_EXTRA_DATA>;
routingPattern?: RoutingPattern;
};
/**
Expand All @@ -151,22 +151,25 @@ export interface IReactApp<T = unknown> {
*/
hasGetStaticRoutes?: (options: ICallServerMethodOptions, forRouteAtFilePath: string) => Promise<boolean>;


App: React.ComponentType<IReactAppProps<T>>;
App: React.ComponentType<IReactAppProps<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>>;
/**
* Renders the App into an HTML element
*
* @returns a cleanup function
*/
render: (targetElement: HTMLElement, props: IReactAppProps<T>) => Promise<() => void>;
render: (
targetElement: HTMLElement,
props: IReactAppProps<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>,
) => Promise<() => void>;
}
export interface IAppManifest<T = unknown> {
routes: RouteInfo<T>[];
homeRoute?: RouteInfo<T>;
errorRoutes?: RouteInfo<T>[];
export interface IAppManifest<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = undefined> {
extraData: MANIFEST_EXTRA_DATA;
routes: RouteInfo<ROUTE_EXTRA_DATA>[];
homeRoute?: RouteInfo<ROUTE_EXTRA_DATA>;
errorRoutes?: RouteInfo<ROUTE_EXTRA_DATA>[];
}

export interface RouteInfo<T = unknown> {
export interface RouteInfo<ROUTE_EXTRA_DATA = unknown> {
pageModule: string;
pageExportName?: string;
/**
Expand All @@ -183,7 +186,6 @@ export interface RouteInfo<T = unknown> {
*/
hasGetStaticRoutes?: boolean;


/**
* a list of export names of the page that should be editable
* if the page is a function, the UI will edit its return value
Expand All @@ -195,7 +197,7 @@ export interface RouteInfo<T = unknown> {
/**
* any extra data that should be passed to the App component
*/
extraData: T;
extraData: ROUTE_EXTRA_DATA;
path: Array<StaticRoutePart | DynamicRoutePart>;
/**
* readable (and editable) text representation of the path
Expand All @@ -215,8 +217,8 @@ export interface DynamicRoutePart {
name: string;
}

export interface IReactAppProps<T = unknown> {
manifest: IAppManifest<T>;
export interface IReactAppProps<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = unknown> {
manifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>;
importModule: DynamicImport;
uri: string;
setUri: (uri: string) => void;
Expand All @@ -236,22 +238,23 @@ export type DynamicImport = (

export interface IPrepareAppOptions {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onManifestUpdate: (appProps: IAppManifest<any>) => void;
onManifestUpdate: (appProps: IAppManifest<any, any>) => void;
fsApi: FSApi;
}
export interface ICallServerMethodOptions {
fsApi: FSApi;
importModule: DynamicImport;
}
export interface IGetNewPageInfoOptions<T> {
export interface IGetNewPageInfoOptions<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = unknown> {
fsApi: FSApi;
requestedURI: string;
manifest: IAppManifest<T>;
manifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>;
}

export type RoutingPattern = 'file' | 'folder(route)' | 'folder(index)';

export interface IMovePageInfoOptions<T> extends IGetNewPageInfoOptions<T> {
export interface IMovePageInfoOptions<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = unknown>
extends IGetNewPageInfoOptions<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA> {
movedFilePath: string;
}
export interface EditablePointOfInterest {
Expand All @@ -265,4 +268,5 @@ export interface IResults<T> {
errorMessage?: string;
}

export type OmitReactApp<T extends IReactApp<D>, D> = Omit<T, 'render' | 'setupStage' | 'setProps'>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type OmitReactApp<T extends IReactApp<any, any>> = Omit<T, 'render' | 'setupStage' | 'setProps'>;
6 changes: 4 additions & 2 deletions packages/app-core/src/define-app.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { reactErrorHandledRendering } from '@wixc3/react-board/dist/react-error-handled-render';
import { IReactApp, OmitReactApp } from './define-app-types';

export function defineApp<T>(input: OmitReactApp<IReactApp<T>, T>): IReactApp<T> {
const res: IReactApp<T> = {
export function defineApp<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = undefined>(
input: OmitReactApp<IReactApp<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>>,
): IReactApp<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA> {
const res: IReactApp<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA> = {
...input,

async render(target, appProps) {
Expand Down
69 changes: 35 additions & 34 deletions packages/app-core/test-kit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { createMemoryFs, IMemFileSystem } from '@file-services/memory';
import { createRequestResolver } from '@file-services/resolve';
import path from '@file-services/path';
import { IDirectoryContents } from '@file-services/types';
export interface AppDefDriverOptions<T> {
app: IReactApp<T>;
export interface AppDefDriverOptions<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATAU = unknown> {
app: IReactApp<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATAU>;
initialFiles: IDirectoryContents;
evaluatedNodeModules: Record<string, unknown>
evaluatedNodeModules: Record<string, unknown>;
/**
* @default '/app-def.ts'
*/
Expand All @@ -18,22 +18,22 @@ export interface AppDefDriverOptions<T> {
projectPath?: string;
}
type DirListenerObj = { cb: (files: string[]) => void; dirPath: string };
export class AppDefDriver<T> {
export class AppDefDriver<MANIFEST_EXTRA_DATA = unknown, ROUTE_EXTRA_DATA = unknown> {
private fs: IMemFileSystem;
private moduleSystem: ICommonJsModuleSystem;
private dirListeners: Array<DirListenerObj> = [];
private manifestListeners: Set<(manifest: IAppManifest<T>) => void> = new Set();
private manifestListeners: Set<(manifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>) => void> = new Set();
private fileListeners: Record<string, Set<(contents: string | null) => void>> = {};
private exportsListeners: Record<string, Set<(exportNames: string[]) => void>> = {};
private lastManifest: IAppManifest<T> | null = null;
private lastManifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA> | null = null;
private disposeApp?: () => void;
constructor(private options: AppDefDriverOptions<T>) {
constructor(private options: AppDefDriverOptions<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>) {
this.fs = createMemoryFs(options.initialFiles);
const resolver = createRequestResolver({fs: this.fs});
const resolver = createRequestResolver({ fs: this.fs });
this.moduleSystem = createBaseCjsModuleSystem({
dirname: this.fs.dirname,
readFileSync: (filePath) => {
const fileContents = this.fs.readFileSync(filePath, {encoding: 'utf8'});
const fileContents = this.fs.readFileSync(filePath, { encoding: 'utf8' });
if (typeof fileContents !== 'string') {
throw new Error(`No content for: ${filePath}`);
}
Expand All @@ -44,7 +44,7 @@ export class AppDefDriver<T> {
return request;
}
const resolved = resolver(contextPath, request);
return resolved.resolvedFile
return resolved.resolvedFile;
},
globals: {},
});
Expand Down Expand Up @@ -73,7 +73,7 @@ export class AppDefDriver<T> {
addOrUpdateFile(filePath: string, contents: string) {
const existingFile = !!this.fs.existsSync(filePath);
this.fs.writeFileSync(filePath, contents);

if (!existingFile) {
for (const listener of this.dirListeners) {
if (filePath.startsWith(listener.dirPath)) {
Expand Down Expand Up @@ -123,10 +123,10 @@ export class AppDefDriver<T> {
movedFilePath,
});
}
addManifestListener(cb: (manifest: IAppManifest<T>) => void) {
addManifestListener(cb: (manifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>) => void) {
this.manifestListeners.add(cb);
}
removeManifestListener(cb: (manifest: IAppManifest<T>) => void) {
removeManifestListener(cb: (manifest: IAppManifest<MANIFEST_EXTRA_DATA, ROUTE_EXTRA_DATA>) => void) {
this.manifestListeners.delete(cb);
}
private dispatchManifestUpdate() {
Expand All @@ -150,41 +150,45 @@ export class AppDefDriver<T> {
const createProps = (uri: string) => ({
callServerMethod(filePath: string, methodName: string, args: unknown[]) {
return app.callServerMethod!(
{
fsApi,
importModule
},
filePath, methodName, args
{
fsApi,
importModule,
},
filePath,
methodName,
args,
);
},
importModule: this.importModule,
manifest: this.lastManifest!,
onCaughtError() {/**/},
onCaughtError() {
/**/
},
setUri(_uri: string) {
// ToDo: implement
},
uri,
});
const unmount = await app.render(container, createProps(uri));
let lastUri = uri;
const rerender = ({uri = '/'}: {uri?: string} = {})=>{
const rerender = ({ uri = '/' }: { uri?: string } = {}) => {
lastUri = uri;
return app.render(container, createProps(uri));
}
};
const manifestListener = () => {
void rerender({uri: lastUri});
void rerender({ uri: lastUri });
};
if (testAutoRerenderOnManifestUpdate !== false) {
this.addManifestListener(manifestListener);
}
return {
dispose: ()=> {
dispose: () => {
unmount();
container.remove();
this.removeManifestListener(manifestListener)
this.removeManifestListener(manifestListener);
},
container,
rerender
rerender,
};
}
dispose() {
Expand Down Expand Up @@ -215,7 +219,7 @@ export class AppDefDriver<T> {
stop: () => {
listeners.delete(cb);
},
contents: Promise.resolve(this.fs.readFileSync(filePath, {encoding: 'utf8'}) ?? null),
contents: Promise.resolve(this.fs.readFileSync(filePath, { encoding: 'utf8' }) ?? null),
};
},
watchFileExports: (filePath: string, cb) => {
Expand All @@ -227,11 +231,11 @@ export class AppDefDriver<T> {
try {
const module = this.moduleSystem.requireModule(filePath);
moduleExports = Object.keys(module as Record<string, unknown>);
} catch (e){
} catch (e) {
const errMsg = e instanceof Error ? e.message : String(e);
throw new Error(`error requiring module ${filePath}: ${errMsg}`);
}

return {
stop: () => {
listeners.delete(cb);
Expand All @@ -248,11 +252,11 @@ export class AppDefDriver<T> {
} catch (error) {
errorMessage = error instanceof Error ? error.message : String(error);
}
return { module, errorMessage }
return { module, errorMessage };
};
const { stop } = this.fsApi.watchFile(filePath, () => {
this.moduleSystem.moduleCache.delete(filePath);
const {module, errorMessage} = requireModule();
const { module, errorMessage } = requireModule();
onModuleChange?.({
results: module || null,
status: errorMessage ? 'invalid' : 'ready',
Expand All @@ -264,7 +268,7 @@ export class AppDefDriver<T> {
moduleResults: Promise.resolve({
status: errorMessage ? 'invalid' : 'ready',
results: module || null,
errorMessage
errorMessage,
}),
dispose() {
stop();
Expand All @@ -283,7 +287,4 @@ export class AppDefDriver<T> {
}
return nestedPaths;
}

}


Loading

0 comments on commit 5c1023d

Please sign in to comment.