wirejs-deploy-amplify-basic 0.0.1-alpha

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.
@@ -0,0 +1,22 @@
1
+ import { env } from 'process';
2
+
3
+ import { LambdaFunctionURLHandler } from 'aws-lambda';
4
+
5
+ // import {
6
+ // S3Client,
7
+ // ListObjectsCommand,
8
+ // PutObjectCommand,
9
+ // GetObjectCommand
10
+ // } from '@aws-sdk/client-s3';
11
+
12
+ // import * as api from '../src/api/index.js';
13
+
14
+ // const s3 = new S3Client()
15
+
16
+ export const handler: LambdaFunctionURLHandler = async (event, context) => {
17
+ return {
18
+ statusCode: 200,
19
+ cookies: [],
20
+ body: 'Hello.'
21
+ }
22
+ }
@@ -0,0 +1,36 @@
1
+ import {
2
+ defineBackend,
3
+ defineStorage,
4
+ defineFunction
5
+ } from '@aws-amplify/backend';
6
+ import { RemovalPolicy } from "aws-cdk-lib";
7
+ import { FunctionUrlAuthType } from 'aws-cdk-lib/aws-lambda';
8
+
9
+ const api = defineFunction({
10
+ entry: '../api-lambda.ts',
11
+ })
12
+
13
+ const storage = defineStorage({
14
+ name: 'app-data',
15
+ access: allow => allow.resource(api)
16
+ });
17
+
18
+ const backend = defineBackend({
19
+ api,
20
+ storage,
21
+ });
22
+
23
+ backend.api.addEnvironment(
24
+ 'BUCKET', backend.storage.resources.bucket.bucketName
25
+ );
26
+ const apiUrl = backend.api.resources.lambda.addFunctionUrl({
27
+ authType: FunctionUrlAuthType.NONE
28
+ });
29
+
30
+ backend.storage.resources.bucket.applyRemovalPolicy(RemovalPolicy.RETAIN);
31
+
32
+ backend.addOutput({
33
+ custom: {
34
+ api: apiUrl.url
35
+ }
36
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2022",
4
+ "module": "es2022",
5
+ "moduleResolution": "bundler",
6
+ "resolveJsonModule": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "strict": true,
10
+ "skipLibCheck": true,
11
+ "paths": {
12
+ "$amplify/*": [
13
+ "../.amplify/generated/*"
14
+ ]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,183 @@
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ import { JSDOM } from 'jsdom';
6
+ import { useJSDOM } from 'wirejs-dom/v2';
7
+ import { Context, CookieJar } from 'wirejs-resources';
8
+
9
+ const SSR_ROOT = path.join(path.dirname(module.filename), 'ssr');
10
+
11
+ const logger = {
12
+ log(...items) {
13
+ console.log('wirejs', ...items);
14
+ },
15
+ error(...items) {
16
+ console.error('wirejs ERROR', ...items);
17
+ },
18
+ info(...items) {
19
+ console.info('wirejs', ...items);
20
+ },
21
+ warn(...items) {
22
+ console.warn('wirejs', ...items);
23
+ }
24
+ };
25
+
26
+ /**
27
+ * Compare two strings by length for sorting in order of increasing length.
28
+ *
29
+ * @param {string} a
30
+ * @param {string} b
31
+ * @returns
32
+ */
33
+ function byLength(a, b) {
34
+ return a.length - b.length;
35
+ }
36
+
37
+ /**
38
+ * @param {string} pattern - string pattern, where `*` matches anything
39
+ * @param {string} text
40
+ * @returns
41
+ */
42
+ function globMatch(pattern, text) {
43
+ const parts = pattern.split('*');
44
+ const regex = new RegExp(parts.join('.+'));
45
+ return regex.test(text);
46
+ }
47
+
48
+ /**
49
+ *
50
+ * @param {http.IncomingMessage} req
51
+ * @returns
52
+ */
53
+ function createContext(req) {
54
+ const { url, headers } = req;
55
+ const origin = headers.origin || `https://${headers.host}`;
56
+ const location = new URL(`${origin}${url}`);
57
+ const cookies = new CookieJar(headers.cookie);
58
+ return new Context({ cookies, location });
59
+ }
60
+
61
+ /**
62
+ *
63
+ * @param {Context} context
64
+ * @param {string} [forceExt]
65
+ */
66
+ function routeSSR(context, forceExt) {
67
+ const asJSPath = forceExt ?
68
+ context.location.pathname.replace(/\.(\w+)$/, `.${forceExt}`)
69
+ : context.location.pathname
70
+ ;
71
+ const allHandlers = fs.readdirSync(SSR_ROOT, { recursive: true })
72
+ .filter(p => p.endsWith('.js'))
73
+ .map(p => `/${p}`)
74
+ ;
75
+ const matchingHandlers = allHandlers.filter(h => globMatch(h, asJSPath));
76
+ const match = matchingHandlers.sort(byLength).pop();
77
+
78
+ if (match) {
79
+ return path.join(SSR_ROOT, match);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * @param {http.IncomingMessage} req
85
+ * @param {http.ServerResponse} res
86
+ * @returns
87
+ */
88
+ async function trySSRScriptPath(req, res) {
89
+ const context = createContext(req);
90
+ const srcPath = routeSSR(context);
91
+ if (!srcPath) return false;
92
+
93
+ logger.info('SSR handler associated script found', srcPath);
94
+
95
+ res.setHeader('Content-Type', 'text/javascript');
96
+
97
+ try {
98
+ res.end(fs.readFileSync(srcPath));
99
+ } catch {
100
+ res.statusCode = 404;
101
+ res.end("404 - File not found (c)");
102
+ }
103
+
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ *
109
+ * @param {http.IncomingMessage} req
110
+ * @param {http.ServerResponse} res
111
+ * @returns
112
+ */
113
+ async function trySSRPath(req, res) {
114
+ const context = createContext(req);
115
+
116
+ const asJSPath = context.location.pathname.replace(/\.(\w+)$/, '.js');
117
+ const srcPath = routeSSR(context, 'js');
118
+ if (!srcPath) return false;
119
+
120
+ logger.info('SSR handler found', srcPath);
121
+
122
+ try {
123
+ useJSDOM(JSDOM);
124
+ global.self = global.window;
125
+ await import(srcPath);
126
+ const module = self.exports;
127
+ if (typeof module.generate === 'function') {
128
+ const doc = await module.generate(context);
129
+ const doctype = doc.parentNode.doctype?.name || '';
130
+
131
+ let hydrationsFound = 0;
132
+ while (globalThis.pendingDehydrations?.length > 0) {
133
+ globalThis.pendingDehydrations.shift()(doc);
134
+ hydrationsFound++;
135
+ }
136
+
137
+ if (hydrationsFound) {
138
+ const script = doc.parentNode.createElement('script');
139
+ script.src = asJSPath;
140
+ doc.parentNode.body.appendChild(script);
141
+ }
142
+
143
+ res.setHeader('Content-type', 'text/html; charset=utf-8')
144
+ res.end([
145
+ doctype ? `<!doctype ${doctype}>\n` : '',
146
+ doc.outerHTML
147
+ ].join(''));
148
+
149
+ return true;
150
+ } else {
151
+ logger.info('SSR module missing generate function');
152
+ return false;
153
+ }
154
+ } catch (error) {
155
+ logger.error('ssr error', error);
156
+ res.statusCode = 404;
157
+ res.end("404 - File not found (a)");
158
+ }
159
+
160
+ return true;
161
+ }
162
+
163
+ /**
164
+ *
165
+ * @param {http.IncomingMessage} req
166
+ * @param {http.ServerResponse} res
167
+ * @returns
168
+ */
169
+ async function handleRequest(req, res) {
170
+ logger.info('received', JSON.stringify({ url: req.url }, null, 2));
171
+
172
+ if (await trySSRScriptPath(req, res)) return;
173
+ if (await trySSRPath(req, res)) return;
174
+
175
+ // if we've made it this far, we don't have what you're looking for
176
+ res.statusCode = '404';
177
+ res.end('404 - Not found');
178
+ }
179
+
180
+ const server = http.createServer(handleRequest);
181
+ server.listen(3000).on('listening', () => {
182
+ console.log('Listening on port 3000')
183
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "version": 1,
3
+ "framework": { "name": "express", "version": "4.18.2" },
4
+ "routes": [
5
+ {
6
+ "path": "/*.*",
7
+ "target": {
8
+ "kind": "Static",
9
+ "cacheControl": "public, max-age=2"
10
+ },
11
+ "fallback": {
12
+ "kind": "Compute",
13
+ "src": "default"
14
+ }
15
+ }
16
+ ],
17
+ "computeResources": [
18
+ {
19
+ "name": "default",
20
+ "runtime": "nodejs20.x",
21
+ "entrypoint": "index.js"
22
+ }
23
+ ]
24
+ }
package/build.js ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from 'process';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import { rimraf } from 'rimraf';
7
+ import copy from 'recursive-copy';
8
+ import esbuild from 'esbuild';
9
+ import { execSync } from 'child_process';
10
+
11
+ const CWD = process.cwd();
12
+ const SELF_DIR = import.meta.dirname;
13
+ const TEMP_DIR = path.join(SELF_DIR, 'temp');
14
+ const RESOURCE_OVERRIDES_BUILD = path.join(
15
+ TEMP_DIR, 'wirejs-resource-overrides.build.js'
16
+ );
17
+ const PROJECT_API_DIR = path.join(CWD, 'api');
18
+ const PROJECT_DIST_DIR = path.join(CWD, 'dist');
19
+ const BACKEND_DIR = path.join(CWD, 'amplify');
20
+ const HOSTING_DIR = path.join(CWD, '.amplify-hosting');
21
+ const STATIC_DIR = path.join(HOSTING_DIR, 'static');
22
+ const COMPUTE_DIR = path.join(HOSTING_DIR, 'compute', 'default');
23
+
24
+ const [_nodeBinPath, _scriptPath, action] = process.argv;
25
+
26
+ /**
27
+ * establish Amplify deployment directory skeleton, used by Amplify
28
+ * to deploy frontend (hosting) and backend (services).
29
+ */
30
+ async function createSkeleton() {
31
+ console.log("creating skeleton deployment directories")
32
+ await rimraf(HOSTING_DIR);
33
+ await fs.promises.mkdir(HOSTING_DIR, { recursive: true });
34
+ await fs.promises.mkdir(STATIC_DIR, { recursive: true });
35
+ await fs.promises.mkdir(COMPUTE_DIR, { recursive: true });
36
+
37
+ // skeleton for backend assets
38
+ await copy(path.join(SELF_DIR, 'amplify-backend-assets'), path.join(BACKEND_DIR));
39
+
40
+ // skeleton for hosting assets
41
+ await copy(path.join(SELF_DIR, 'amplify-hosting-assets'), path.join(HOSTING_DIR));
42
+ console.log("done creating deployment directories")
43
+ }
44
+
45
+ /**
46
+ * Install all deps, adding those needed for Amplify backend deployments.
47
+ */
48
+ async function installDeps() {
49
+ console.log("adding deps to package.json");
50
+ // add deps used by Amplify to deploy backend
51
+ const packageData = JSON.parse(await fs.promises.readFile(path.join(CWD, 'package.json')));
52
+ packageData.devDependencies = {
53
+ ...(packageData.devDependencies || {}),
54
+ ...{
55
+ '@aws-amplify/backend': '^1.14.0',
56
+ '@aws-amplify/backend-cli': '^1.4.8',
57
+ 'aws-cdk': '^2.177.0',
58
+ 'aws-cdk-lib': '^2.177.0',
59
+ 'constructs': '^10.4.2',
60
+ 'esbuild': '^0.24.2',
61
+ 'tsx': '^4.19.2',
62
+ 'typescript': '^5.7.3',
63
+ '@aws-sdk/client-s3': "^3.735.0",
64
+ }
65
+ };
66
+ await fs.promises.writeFile(
67
+ path.join(CWD, 'package.json'),
68
+ JSON.stringify(packageData, null, 2)
69
+ );
70
+
71
+ console.log("installing all deps")
72
+
73
+ // install all
74
+ execSync('npm i');
75
+
76
+ console.log("done installing deps")
77
+ }
78
+
79
+ /**
80
+ *
81
+ * @returns {Promise<string>} output filename
82
+ */
83
+ async function buildApiBundle() {
84
+ console.log("building api")
85
+ // looks like cruft at the moment, but we'll see ...
86
+ // await import(path.join(PROJECT_API_DIR, 'index.js'));
87
+
88
+ const outputPath = path.join(PROJECT_DIST_DIR, 'api', 'dist', 'index.js');
89
+
90
+ // intermediate build of the resource overrides. this includes any deps we have
91
+ // on the original `wirejs-resources` into the intermediate bundle. doing this
92
+ // allows us to completely override (alias) `wirejs-resources` in the final build
93
+ // without creating a circular alias.
94
+ console.log("creating intermediate wirejs-resources overrides");
95
+ await esbuild.build({
96
+ entryPoints: [path.join(SELF_DIR, 'wirejs-resources-overrides', 'index.js')],
97
+ bundle: true,
98
+ outfile: RESOURCE_OVERRIDES_BUILD,
99
+ platform: 'node',
100
+ format: 'esm',
101
+ });
102
+
103
+ // exploratory build. builds using our overrides, which will emit a manifest of
104
+ // resources required by the API when imported.
105
+ console.log("creating api bundle using platform overrides");
106
+ await esbuild.build({
107
+ entryPoints: [path.join('.', 'api', 'index.js')],
108
+ bundle: true,
109
+ outfile: outputPath,
110
+ platform: 'node',
111
+ format: 'esm',
112
+ alias: {
113
+ 'wirejs-resources': RESOURCE_OVERRIDES_BUILD
114
+ }
115
+ });
116
+
117
+ // exploratory import. not strictly necessary until we're actually using the manifest
118
+ // to direct construction of backend resources. for now, this is just informational/
119
+ // confirmational that we're building things properly.
120
+ await import(outputPath);
121
+ console.log('discovered resources', globalThis.wirejsResources);
122
+
123
+ return outputPath;
124
+ }
125
+
126
+ async function deployFrontend() {
127
+ console.log("copying frontend assets");
128
+ await copy(PROJECT_DIST_DIR, STATIC_DIR);
129
+
130
+ // ssr will likely have been build as a static asset, but should NOT be
131
+ // served as a static resource.
132
+ await rimraf(path.join(STATIC_DIR, 'ssr'));
133
+
134
+ // instead, ssr should be served from the "compute" directory, served by lambda@edge
135
+ await copy(
136
+ path.join(PROJECT_DIST_DIR, 'ssr'),
137
+ path.join(HOSTING_DIR, 'compute', 'default', 'ssr')
138
+ );
139
+ console.log('frontend assets copied');
140
+ }
141
+
142
+
143
+ if (action === 'prebuild') {
144
+ console.log("starting prebuild");
145
+ await createSkeleton();
146
+ await installDeps();
147
+ await buildApiBundle();
148
+ console.log("prebuild done");
149
+ } else if (action === 'inject-backend') {
150
+ console.log("starting inject-backend");
151
+ const config = JSON.parse(await fs.promises.readFile(path.join('.', 'amplify_outputs.json')));
152
+ const apiUrl = config.custom.api;
153
+
154
+ const configJSON = JSON.stringify({
155
+ apiUrl
156
+ });
157
+
158
+ await fs.promises.writeFile(
159
+ path.join(PROJECT_API_DIR, 'config.js'),
160
+ `const config = ${configJSON};\nexport default config;`
161
+ );
162
+ console.log("inject-backend done");
163
+ } else if (action === 'build-hosting-artifacts') {
164
+ console.log("starting build-hosting-artifacts");
165
+ await deployFrontend();
166
+ console.log("build-hosting-artifacts done");
167
+ } else {
168
+ throw new Error("Unrecognized action.");
169
+ }
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "wirejs-deploy-amplify-basic",
3
+ "version": "0.0.1-alpha",
4
+ "type": "module",
5
+ "bin": {
6
+ "wirejs-deploy-amplify-basic": "./build.js"
7
+ },
8
+ "dependencies": {
9
+ "copy": "^0.3.2",
10
+ "esbuild": "^0.24.2",
11
+ "recursive-copy": "^2.0.14",
12
+ "rimraf": "^6.0.1",
13
+ "wirejs-resources": "^0.1.5-alpha"
14
+ },
15
+ "devDependencies": {
16
+ "@aws-amplify/backend": "^1.14.0",
17
+ "@aws-sdk/client-s3": "^3.735.0"
18
+ }
19
+ }
@@ -0,0 +1,51 @@
1
+ import { overrides } from 'wirejs-resources';
2
+ import { Resource } from 'wirejs-resources';
3
+
4
+ export {
5
+ AuthenticationService,
6
+ withContext,
7
+ requiresContext,
8
+ Context,
9
+ CookieJar,
10
+ Resource,
11
+ overrides,
12
+ } from 'wirejs-resources';
13
+
14
+ export class FileService extends Resource {
15
+ constructor(scope, id) {
16
+ super(scope, id);
17
+ addResource('FileService', { absoluteId: this.absoluteId });
18
+ }
19
+
20
+ async write(...args) {
21
+ console.log('"writing secret" ... :/ ... ');
22
+ }
23
+ }
24
+
25
+ // expose resources to other resources that might depend on it.
26
+ overrides.FileService = FileService;
27
+
28
+ // export class AuthenticationService {
29
+ // constructor(id) {
30
+ // addResource('AuthenticationService', { id });
31
+ // }
32
+
33
+ // buildApi(...args) {
34
+ // // console.log('AuthService.buildApi', [args]);
35
+ // }
36
+ // }
37
+
38
+ // export class Secret {
39
+ // constructor(id) {
40
+ // addResource('Secret', { id });
41
+ // }
42
+ // }
43
+
44
+ globalThis.wirejsResources = [];
45
+
46
+ function addResource(type, options) {
47
+ wirejsResources.push({
48
+ type,
49
+ options
50
+ });
51
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "wirejs-resources-overrides",
3
+ "version": "0.0.1",
4
+ "description": "Basic services and server-side resources for wirejs apps",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./index.js"
10
+ }
11
+ }
12
+ }