Skip to content

Commit 8b1a9e7

Browse files
committed
Bumping everything, transparent cookie sync in Next.js
1 parent 7f2be1c commit 8b1a9e7

File tree

14 files changed

+1231
-1397
lines changed

14 files changed

+1231
-1397
lines changed

package-lock.json

Lines changed: 937 additions & 1057 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"nuxt3",
2121
"ssr"
2222
],
23+
"sideEffects": false,
2324
"scripts": {
2425
"build": "rm -rf dist && tsc && ts-node ./tools/build.ts",
2526
"test": "echo \"Error: no test specified\" && exit 1",
@@ -39,10 +40,20 @@
3940
"node": "./dist/index.js",
4041
"default": null
4142
},
43+
"./tools": {
44+
"node": "./dist/tools.js",
45+
"default": null
46+
},
4247
"./server": {
4348
"node": "./dist/server/index.js",
4449
"default": null
45-
}
50+
},
51+
"./server/firebase-aware": {
52+
"node": "./dist/server/firebase-aware.js",
53+
"default": null
54+
},
55+
"./client/auth": "./dist/client/auth/index.js",
56+
"./client/app": "./dist/client/app/index.js"
4657
},
4758
"files": [
4859
"dist"
@@ -51,6 +62,7 @@
5162
"dependencies": {
5263
"fs-extra": "^10.1.0",
5364
"jsonc-parser": "^3.0.0",
65+
"semver": "^7.3.7",
5466
"tslib": "^2.3.1"
5567
},
5668
"devDependencies": {
@@ -62,8 +74,9 @@
6274
"@types/inquirer": "^8.1.3",
6375
"@types/lru-cache": "^7.6.1",
6476
"@types/node": "^16.11.12",
77+
"@types/semver": "^7.3.9",
6578
"cookie": "^0.5.0",
66-
"firebase": "^9.6.8",
79+
"firebase": "^9.7.1-20220505222723",
6780
"firebase-admin": "^10.1.0",
6881
"firebase-functions": "^3.20.1",
6982
"firebase-tools": "^10.5.0",

src/client/auth/index.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
export * from 'firebase/auth';
2+
import {
3+
beforeAuthStateChanged,
4+
onIdTokenChanged,
5+
getAuth as getFirebaseAuth,
6+
initializeAuth as initializeFirebaseAuth,
7+
User,
8+
Auth,
9+
Dependencies
10+
} from 'firebase/auth';
11+
import type { FirebaseApp } from 'firebase/app';
12+
import { ID_TOKEN_MAX_AGE } from '../../constants';
13+
14+
let alreadySetup = false;
15+
let lastPostedIdToken: string|undefined;
16+
17+
const mintCookie = async (user: User|null) => {
18+
const idTokenResult = user && await user.getIdTokenResult();
19+
const idTokenAge = idTokenResult && (new Date().getTime() - Date.parse(idTokenResult.issuedAtTime)) / 1_000;
20+
if (idTokenAge && idTokenAge > ID_TOKEN_MAX_AGE) return;
21+
const idToken = idTokenResult?.token;
22+
if (lastPostedIdToken === idToken) return;
23+
lastPostedIdToken = idToken;
24+
await fetch('/__session', {
25+
method: idToken ? 'POST' : 'DELETE',
26+
headers: idToken ? {
27+
'Authorization': `Bearer ${idToken}`,
28+
} : {}
29+
});
30+
};
31+
32+
const setup = (auth: Auth) => {
33+
if (auth.app.name !== '[DEFAULT]') return;
34+
if (typeof window === 'undefined') return;
35+
if (alreadySetup) return;
36+
alreadySetup = true;
37+
beforeAuthStateChanged(auth, mintCookie, () => {
38+
mintCookie(auth.currentUser)
39+
});
40+
onIdTokenChanged(auth, user => mintCookie(user));
41+
}
42+
43+
export function getAuth(app?: FirebaseApp) {
44+
console.log('getFirebaseAuth');
45+
const auth = getFirebaseAuth(app);
46+
setup(auth);
47+
return auth;
48+
}
49+
50+
export function initializeAuth(app: FirebaseApp, deps?: Dependencies) {
51+
console.log('initializeFirebaseAuth');
52+
const auth = initializeFirebaseAuth(app, deps);
53+
setup(auth);
54+
return auth;
55+
}

src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const FIREBASE_ADMIN_VERSION = '__FIREBASE_ADMIN_VERSION__';
2+
export const FIREBASE_FUNCTIONS_VERSION = '__FIREBASE_FUNCTIONS_VERSION__';
3+
export const COOKIE_VERSION = '__COOKIE_VERSION__';
4+
export const LRU_CACHE_VERSION = '__LRU_CACHE_VERSION__';
5+
export const FIREBASE_FRAMEWORKS_VERSION = '__FIREBASE_FRAMEWORKS_VERSION__';
6+
export const DEFAULT_REGION = 'us-central1';
7+
export const LRU_MAX_INSTANCES = 100;
8+
export const LRU_TTL = 1_000 * 60 * 5;
9+
export const ID_TOKEN_MAX_AGE = 5 * 60;
10+
export const COOKIE_MAX_AGE = 60 * 60 * 24 * 5 * 1_000;

src/firebase.ts

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/frameworks/index.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
import { spawnSync } from 'child_process';
12
import { existsSync } from 'fs';
2-
import { rm } from 'fs/promises';
3+
import { copyFile, rm, stat, writeFile } from 'fs/promises';
4+
import { basename, join, relative } from 'path';
35

4-
import { DeployConfig, PathFactory } from '../utils';
6+
import {
7+
DEFAULT_REGION,
8+
COOKIE_VERSION,
9+
FIREBASE_ADMIN_VERSION,
10+
FIREBASE_FRAMEWORKS_VERSION,
11+
FIREBASE_FUNCTIONS_VERSION,
12+
LRU_CACHE_VERSION
13+
} from '../constants';
14+
import { DeployConfig, findDependency, PathFactory } from '../utils';
15+
16+
const NODE_VERSION = parseInt(process.versions.node, 10).toString();
517

618
const dynamicImport = (getProjectPath: PathFactory) => {
719
const exists = (...files: string[]) => files.some(file => existsSync(getProjectPath(file)));
@@ -14,5 +26,82 @@ const dynamicImport = (getProjectPath: PathFactory) => {
1426
export const build = async (config: DeployConfig | Required<DeployConfig>, getProjectPath: PathFactory) => {
1527
const command = await dynamicImport(getProjectPath);
1628
await rm(config.dist, { recursive: true, force: true });
17-
return command.build(config, getProjectPath);
29+
const results = await command.build(config, getProjectPath);
30+
const { usingCloudFunctions, packageJson, framework, bootstrapScript, rewrites, redirects, headers } = results;
31+
let usesFirebaseConfig = false;
32+
if (usingCloudFunctions) {
33+
const firebaseAuthDependency = findDependency('@firebase/auth', getProjectPath());
34+
usesFirebaseConfig = !!firebaseAuthDependency;
35+
36+
packageJson.main = 'server.js';
37+
delete packageJson.devDependencies;
38+
packageJson.dependencies ||= {};
39+
packageJson.dependencies['firebase-frameworks'] = FIREBASE_FRAMEWORKS_VERSION;
40+
const functionsDist = join(config.dist, 'functions');
41+
for (const [name, version] of Object.entries(packageJson.dependencies as Record<string, string>)) {
42+
if (version.startsWith('file:')) {
43+
const path = version.split(':')[1];
44+
const stats = await stat(path);
45+
if (stats.isDirectory()) {
46+
const result = spawnSync('npm', ['pack', relative(functionsDist, path)], { cwd: functionsDist });
47+
if (!result.stdout) continue;
48+
const filename = result.stdout.toString().trim();
49+
packageJson.dependencies[name] = `file:${filename}`;
50+
} else {
51+
const filename = basename(path);
52+
await copyFile(path, join(functionsDist, filename));
53+
packageJson.dependencies[name] = `file:${filename}`;
54+
}
55+
}
56+
}
57+
58+
// TODO(jamesdaniels) test these with semver, error if already set out of range
59+
packageJson.dependencies['firebase-admin'] ||= FIREBASE_ADMIN_VERSION;
60+
packageJson.dependencies['firebase-functions'] ||= FIREBASE_FUNCTIONS_VERSION;
61+
if (usesFirebaseConfig) {
62+
packageJson.dependencies['cookie'] ||= COOKIE_VERSION;
63+
packageJson.dependencies['lru-cache'] ||= LRU_CACHE_VERSION;
64+
}
65+
packageJson.engines ||= {};
66+
packageJson.engines.node ||= NODE_VERSION;
67+
68+
await writeFile(join(functionsDist, 'package.json'), JSON.stringify(packageJson, null, 2));
69+
70+
await copyFile(getProjectPath('package-lock.json'), join(functionsDist, 'package-lock.json')).catch(() => {});
71+
72+
const npmInstall = spawnSync('npm', ['i', '--only', 'production', '--no-audit', '--silent'], { cwd: functionsDist });
73+
if (npmInstall.status) {
74+
console.error(npmInstall.output.toString());
75+
}
76+
77+
// TODO(jamesdaniels) allow configuration of the Cloud Function
78+
await writeFile(join(functionsDist, 'settings.js'), `exports.HTTPS_OPTIONS = {};
79+
exports.FRAMEWORK = '${framework}';
80+
`);
81+
82+
if (bootstrapScript) {
83+
await writeFile(join(functionsDist, 'bootstrap.js'), bootstrapScript);
84+
}
85+
if (usesFirebaseConfig) {
86+
await writeFile(join(functionsDist, 'server.js'), "exports.ssr = require('firebase-frameworks/server/firebase-aware').ssr;\n");
87+
} else {
88+
await writeFile(join(functionsDist, 'server.js'), "exports.ssr = require('firebase-frameworks/server').ssr;\n");
89+
}
90+
91+
await writeFile(join(functionsDist, 'functions.yaml'), JSON.stringify({
92+
endpoints: {
93+
[config.function!.name]: {
94+
platform: 'gcfv2',
95+
region: [DEFAULT_REGION],
96+
labels: {},
97+
httpsTrigger: {},
98+
entryPoint: 'ssr'
99+
}
100+
},
101+
specVersion: 'v1alpha1',
102+
// TODO(jamesdaniels) add persistent disk if needed
103+
requiredAPIs: []
104+
}, null, 2));
105+
}
106+
return { usingCloudFunctions, rewrites, redirects, headers, usesFirebaseConfig };
18107
};

src/frameworks/next.js/index.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,18 @@
1313
// limitations under the License.
1414

1515
import { readFile, mkdir, copyFile, stat } from 'fs/promises';
16-
import { dirname, extname, join } from 'path';
17-
import type { NextConfig } from 'next/dist/server/config-shared';
16+
import { dirname, extname, join, relative, resolve, sep } from 'path';
1817
import type { Header, Rewrite, Redirect } from 'next/dist/lib/load-custom-routes';
18+
import type { NextConfig } from 'next';
1919
import nextBuild from 'next/dist/build';
2020
import { copy } from 'fs-extra';
21+
import * as webpack from 'webpack';
22+
import { lt } from 'semver';
2123

22-
import { DeployConfig, PathFactory, exec } from '../../utils';
24+
import { DeployConfig, PathFactory, exec, findDependency } from '../../utils';
2325

2426
export const build = async (config: DeployConfig | Required<DeployConfig>, getProjectPath: PathFactory) => {
2527

26-
await nextBuild(getProjectPath(), null, false, false, true);
27-
// TODO be a bit smarter about this
28-
await exec(`${getProjectPath('node_modules', '.bin', 'next')} export`, { cwd: getProjectPath() }).catch(() => {});
29-
3028
let nextConfig: NextConfig;
3129
try {
3230
const { default: loadConfig }: typeof import('next/dist/server/config') = require(getProjectPath('node_modules', 'next', 'dist', 'server', 'config'));
@@ -37,6 +35,37 @@ export const build = async (config: DeployConfig | Required<DeployConfig>, getPr
3735
nextConfig = await import(getProjectPath('next.config.js'));
3836
}
3937

38+
let usesFirebaseConfig = false;
39+
const firebaseDependency = findDependency('firebase', getProjectPath());
40+
let overrideConfig: NextConfig|null = null;
41+
if (firebaseDependency) {
42+
overrideConfig = {
43+
...nextConfig,
44+
webpack: (config, context) => {
45+
let newConfig = config;
46+
if (nextConfig.webpack) newConfig = nextConfig.webpack(config, context);
47+
const plugin = new webpack.NormalModuleReplacementPlugin(/^firebase\/(auth)$/, (resource: any) => {
48+
// Don't allow firebase-frameworks to recurse
49+
const frameworksRoot = resolve(`${dirname(require.resolve('../..'))}${sep}..`);
50+
if (resource.context.startsWith(frameworksRoot)) return;
51+
// Don't mutate their node_modules
52+
if (relative(getProjectPath(), resource.context).startsWith(`node_modules${sep}`)) return;
53+
const client = resource.request.split('firebase/')[1];
54+
// auth requires beforeAuthStateChanged, released in 9.7.1
55+
if (client === 'auth' && lt(firebaseDependency.version, '9.7.1')) return;
56+
resource.request = require.resolve(`../../client/${client}`);
57+
});
58+
newConfig.plugins ||= [];
59+
newConfig.plugins.push(plugin);
60+
return newConfig;
61+
}
62+
}
63+
}
64+
65+
await nextBuild(getProjectPath(), overrideConfig as any, false, false, true);
66+
// TODO be a bit smarter about this
67+
await exec(`${getProjectPath('node_modules', '.bin', 'next')} export`, { cwd: getProjectPath() }).catch(() => {});
68+
4069
// SEMVER these defaults are only needed for Next 11
4170
const { distDir='.next', basePath='' } = nextConfig;
4271

@@ -123,7 +152,7 @@ export const build = async (config: DeployConfig | Required<DeployConfig>, getPr
123152
return { source, destination };
124153
}).filter(it => it);
125154

126-
return { usingCloudFunctions, headers, redirects, rewrites, framework: 'next.js', packageJson, bootstrapScript: null };
155+
return { usingCloudFunctions, usesFirebaseConfig, headers, redirects, rewrites, framework: 'next.js', packageJson, bootstrapScript: null };
127156
}
128157

129158

0 commit comments

Comments
 (0)