Skip to content

Commit

Permalink
feat: ESM support.
Browse files Browse the repository at this point in the history
  • Loading branch information
amiller-gh committed Jun 25, 2022
1 parent b421815 commit 260f1bd
Show file tree
Hide file tree
Showing 7 changed files with 463 additions and 569 deletions.
15 changes: 6 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "loll",
"version": "0.2.2",
"version": "1.0.0",
"description": "REST apps for the lazy developer.",
"main": "dist/src/index.js",
"type": "module",
"scripts": {
"build": "rm -rf dist && tsc --jsx preserve -p tsconfig.json",
"pretest": "yarn run build",
Expand All @@ -28,18 +29,14 @@
},
"homepage": "https://github.com/amiller-gh/loll#readme",
"dependencies": {
"chalk": "^3.0.0",
"walk": "^2.3.9"
"chalk": "^3.0.0"
},
"devDependencies": {
"@types/express": "^4.17.2",
"@types/mocha": "^5.2.7",
"@types/node-fetch": "^2.5.4",
"@types/walk": "^2.3.0",
"@types/mocha": "^9.1.1",
"express": "^4.17.1",
"mocha": "^6.2.2",
"node-fetch": "^2.6.0",
"typescript": "^3.7.4",
"mocha": "^10.0.0",
"typescript": "^4.7.4",
"watch": "^1.0.2"
}
}
120 changes: 55 additions & 65 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as path from 'path';
import chalk from 'chalk';
import * as url from 'url';

import * as Express from 'express';
import * as walk from 'walk';
import * as chalk from 'chalk';
import type { Dirent } from 'fs';
import type Express from 'express';

import walk from './walk.js';

const DEFAULT_API_ROOT = path.join(process.cwd(), 'api');

Expand Down Expand Up @@ -69,8 +72,8 @@ const evalAPI = function(ctx: any, func: Express.RequestHandler) {
};

// My own custom "interop default" function. Fancy.
function getHandler(filePath: string): IApiHandler | IApiConstructor {
const obj = require(filePath);
async function getHandler(filePath: string): Promise<IApiHandler | IApiConstructor> {
const obj = await import(url.pathToFileURL(filePath).toString());
return typeof obj === 'object' && obj.default ? obj.default : obj;
}

Expand All @@ -88,10 +91,10 @@ function hasValidMethod(handler: IApiHandler){
)
}

function loadAPI(router: Express.Router, filePath: string, apiPath: string){
async function loadAPI(router: Express.Router, filePath: string, apiPath: string) {
let methods = '';
try {
let handler = getHandler(filePath);
let handler = await getHandler(filePath);

// If handler is a function, lets assume its a class constructor.
if (isApiConstructor(handler)) { handler = new handler(router); }
Expand Down Expand Up @@ -122,63 +125,50 @@ interface QueueItem {
filePath: string;
}

function discoverAPI(router: Express.Router, apiDir: string){
var queue: QueueItem[] = [],
options = {
listeners: {
file: function (root: string, fileStats: walk.WalkStats, next: walk.WalkNext) {
// Ignore hidden files
if(fileStats.name[0] === '.' || fileStats.name[0] === '_' || !fileStats.name.endsWith('.js')) return next();

// Construct both the absolute file path, and public facing API path
const filePath = path.join(root, fileStats.name);
const apiPath = path.join(root, fileStats.name).split(path.sep).map((part) => {
if (part.startsWith('[') && part.endsWith(']')) {
part = `:${part.slice(1, -1)}`;
}

if (part.startsWith('(') && part.endsWith(')')) {
part = `:${part.slice(1, -1)}?`;
}

if (part.startsWith('[') && part.endsWith('].js')) {
part = `:${part.slice(1, -4)}.js`;
}

if (part.startsWith('(') && part.endsWith(').js')) {
part = `:${part.slice(1, -4)}?.js`;
}

return part;
}).join(path.sep).replace(apiDir, '').replace(/\/index.js$/, '').replace(/.js$/, '');

// Push them to our queue. This later sorted in order of route precedence.
queue.push({ apiPath, filePath });

// Process next file
next();
},

end: function () {
// Sort queue in reverse alphabetical order.
// Has the nice side effect of ordering by route precedence
queue.sort(function(file1, file2){
return (file1.apiPath > file2.apiPath) ? 1 : -1;
})

// For each API item in the queue, load it into our router
while(queue.length){
const file = queue.pop()!; // TODO: When ES6 is common in node, make let
loadAPI(router, file.filePath, file.apiPath);
}

// When we have loaded all of our API endpoints, register our catchall route
router.all('*', apiNotFound);
}
}
};
async function discoverAPI(router: Express.Router, apiDir: string){
var queue: QueueItem[] = [];
try{
walk.walkSync(apiDir, options);
walk(apiDir, (root: string, fileStats: Dirent | Error) => {
if (fileStats instanceof Error) { return; }
// Ignore hidden files
if(fileStats.name[0] === '.' || fileStats.name[0] === '_' || !fileStats.name.endsWith('.js')) return;

// Construct both the absolute file path, and public facing API path
const filePath = path.join(root, fileStats.name);
const apiPath = path.join(root, fileStats.name).split(path.sep).map((part) => {
if (part.startsWith('[') && part.endsWith(']')) {
part = `:${part.slice(1, -1)}`;
}

if (part.startsWith('(') && part.endsWith(')')) {
part = `:${part.slice(1, -1)}?`;
}

if (part.startsWith('[') && part.endsWith('].js')) {
part = `:${part.slice(1, -4)}.js`;
}

if (part.startsWith('(') && part.endsWith(').js')) {
part = `:${part.slice(1, -4)}?.js`;
}
return part;
}).join(path.sep).replace(apiDir, '').replace(/\/index.js$/, '').replace(/.js$/, '');
queue.push({ filePath, apiPath });
})
// Sort queue in reverse alphabetical order.
// Has the nice side effect of ordering by route precedence
queue.sort(function(file1, file2){
return (file1.apiPath > file2.apiPath) ? 1 : -1;
})

// For each API item in the queue, load it into our router
while(queue.length){
const file = queue.pop()!; // TODO: When ES6 is common in node, make let
await loadAPI(router, file.filePath, file.apiPath);
}

// When we have loaded all of our API endpoints, register our catchall route
router.all('*', apiNotFound);
} catch(e){
console.error(chalk.bold.red('✘ Error reading API directory: '), e);
}
Expand Down Expand Up @@ -226,7 +216,7 @@ async function ApiQuery(router: Express.Router, method: Method, req: Express.Req
// Creates a new express Router using the parent application's version of express
// And adds a middleware that attaches a new instance of the api query function
// to each request's locals object.
export default function loll(express: any, options: LollOptions = {}) {
export default async function loll(express: any, options: LollOptions = {}) {
const app = express() as Express.Express;
const router = express.Router() as Express.Router;

Expand All @@ -250,7 +240,7 @@ export default function loll(express: any, options: LollOptions = {}) {
});

console.log(chalk.bold.green('• Discovering API:'));
discoverAPI(router, options.root || DEFAULT_API_ROOT);
await discoverAPI(router, options.root || DEFAULT_API_ROOT);
app.use(router);
console.log(chalk.bold.green('✔ API Discovery Complete'));
return app;
Expand Down
24 changes: 24 additions & 0 deletions src/walk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { lstatSync, readdirSync, Dirent } from 'fs';
import { join, basename, resolve, dirname } from 'path';

export type WalkFunc = (pathName: string, dirent: Dirent | Error) => Boolean | void;

function lstatSafe(pathName: string): Dirent | Error {
try { const result = lstatSync(pathName) as unknown as Dirent; result.name = basename(resolve(pathName)); return result; }
catch (err) { return err }
}

function readdirSafe(pathName: string): Dirent[] | Error[] {
try { return readdirSync(pathName, { withFileTypes: true }); }
catch (err) { return [err] }
}

function walkDeep(pathName: string, walkFunc: WalkFunc, dirent: Dirent | Error) {
// If the callback returns false, the Dirent errored, or entry is a file, we can skip. Otherwise, walk our directory.
if (walkFunc(dirname(pathName), dirent) === false || dirent instanceof Error || !dirent.isDirectory()) { return; }
for (const entry of readdirSafe(pathName)) { walkDeep(join(pathName, entry.name), walkFunc, entry) }
}

// Get the very first file or folder an walk deep.
const walk = (pathName: string, walkFunc: WalkFunc) => walkDeep(pathName, walkFunc, lstatSafe(pathName));
export default walk;
10 changes: 5 additions & 5 deletions test/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import * as assert from 'assert';
import * as http from 'http';
import * as path from 'path';
import express from 'express';
import { fileURLToPath } from 'url';

import * as express from 'express';
import fetch, { Response } from 'node-fetch';

import loll from '../src';
import loll from '../src/index.js';

const PORT = 3030;
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const GET = async (url: string, obj: any): Promise<Response> => {
const res = await fetch(`http://localhost:${PORT}${url}`, { method: 'GET' });
Expand All @@ -21,7 +21,7 @@ const DELETE = async (url: string, obj: any) => assert.deepStrictEqual(await fet
const ERROR_RESPONSE = { code: 400, status: 'error', message: 'Method Not Implemented' };

const app = express();
const api = loll(express, {
const api = await loll(express, {
root: path.join(__dirname, 'fixtures', 'discovery'),
});
app.use('/api', api);
Expand Down
12 changes: 5 additions & 7 deletions test/sideways.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import * as assert from 'assert';
import * as http from 'http';
import * as path from 'path';
import express from 'express';
import { fileURLToPath } from 'url';

import * as express from 'express';
import fetch from 'node-fetch';

import loll from '../src';
import loll from '../src/index.js';

const PORT = 3031;
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const GET = async (url: string, obj: any) => assert.deepStrictEqual(await fetch(`http://localhost:${PORT}${url}`, { method: 'GET' }).then(res => res.json()), obj);
// const POST = async (url: string, body: any, obj: any) => assert.deepStrictEqual(await fetch(`http://localhost:3000${url}`, { method: 'POST', body }).then(res => res.json()), obj);
Expand All @@ -17,9 +17,7 @@ const GET = async (url: string, obj: any) => assert.deepStrictEqual(await fetch(
// const ERROR_RESPONSE = { code: 400, status: 'error', message: 'Method Not Implemented' };

const app = express();
const api = loll(express, {
root: path.join(__dirname, 'fixtures', 'sideways'),
});
const api = await loll(express, { root: path.join(__dirname, 'fixtures', 'sideways') });
app.use('/api', api);

// Start Server
Expand Down
8 changes: 3 additions & 5 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
{
"include": [ "src/**/*", "test/**/*" ],
"compilerOptions": {

"outDir": "dist",
"baseUrl": "dist",
"lib": [ "es7", "es2017", "dom" ],
"lib": [ "esnext", "dom" ],

// JSX Settings for Preact
"jsx": "react",
"jsxFactory": "h",

// Compilation Configuration
"target": "es2015",
"target": "esnext",
"inlineSources": true,
"sourceMap": true,
"declaration": true,
Expand Down Expand Up @@ -40,8 +39,7 @@
"preserveConstEnums": false,
"newLine": "LF",
"traceResolution": false,
"module": "commonjs",

"module": "esnext",
},

"references": []
Expand Down
Loading

0 comments on commit 260f1bd

Please sign in to comment.